[
  {
    "path": ".beads/.gitignore",
    "content": "# SQLite databases\n*.db\n*.db?*\n*.db-journal\n*.db-wal\n*.db-shm\n\n# Daemon runtime files\ndaemon.lock\ndaemon.log\ndaemon.pid\nbd.sock\nsync-state.json\nlast-touched\n\n# Local version tracking (prevents upgrade notification spam after git ops)\n.local_version\n\n# Legacy database files\ndb.sqlite\nbd.db\n\n# Worktree redirect file (contains relative path to main repo's .beads/)\n# Must not be committed as paths would be wrong in other clones\nredirect\n\n# Merge artifacts (temporary files from 3-way merge)\nbeads.base.jsonl\nbeads.base.meta.json\nbeads.left.jsonl\nbeads.left.meta.json\nbeads.right.jsonl\nbeads.right.meta.json\n\n# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here.\n# They would override fork protection in .git/info/exclude, allowing\n# contributors to accidentally commit upstream issue databases.\n# The JSONL files (issues.jsonl, interactions.jsonl) and config files\n# are tracked by git by default since no pattern above ignores them.\n"
  },
  {
    "path": ".beads/README.md",
    "content": "# Beads - AI-Native Issue Tracking\n\nWelcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code.\n\n## What is Beads?\n\nBeads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git.\n\n**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads)\n\n## Quick Start\n\n### Essential Commands\n\n```bash\n# Create new issues\nbd create \"Add user authentication\"\n\n# View all issues\nbd list\n\n# View issue details\nbd show <issue-id>\n\n# Update issue status\nbd update <issue-id> --status in_progress\nbd update <issue-id> --status done\n\n# Sync with git remote\nbd sync\n```\n\n### Working with Issues\n\nIssues in Beads are:\n- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code\n- **AI-friendly**: CLI-first design works perfectly with AI coding agents\n- **Branch-aware**: Issues can follow your branch workflow\n- **Always in sync**: Auto-syncs with your commits\n\n## Why Beads?\n\n✨ **AI-Native Design**\n- Built specifically for AI-assisted development workflows\n- CLI-first interface works seamlessly with AI coding agents\n- No context switching to web UIs\n\n🚀 **Developer Focused**\n- Issues live in your repo, right next to your code\n- Works offline, syncs when you push\n- Fast, lightweight, and stays out of your way\n\n🔧 **Git Integration**\n- Automatic sync with git commits\n- Branch-aware issue tracking\n- Intelligent JSONL merge resolution\n\n## Get Started with Beads\n\nTry Beads in your own projects:\n\n```bash\n# Install Beads\ncurl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash\n\n# Initialize in your repo\nbd init\n\n# Create your first issue\nbd create \"Try out Beads\"\n```\n\n## Learn More\n\n- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs)\n- **Quick Start Guide**: Run `bd quickstart`\n- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples)\n\n---\n\n*Beads: Issue tracking that moves at the speed of thought* ⚡\n"
  },
  {
    "path": ".beads/config.yaml",
    "content": "# Beads Configuration File\n# This file configures default behavior for all bd commands in this repository\n# All settings can also be set via environment variables (BD_* prefix)\n# or overridden with command-line flags\n\n# Issue prefix for this repository (used by bd init)\n# If not set, bd init will auto-detect from directory name\n# Example: issue-prefix: \"myproject\" creates issues like \"myproject-1\", \"myproject-2\", etc.\n# issue-prefix: \"\"\n\n# Use no-db mode: load from JSONL, no SQLite, write back after each command\n# When true, bd will use .beads/issues.jsonl as the source of truth\n# instead of SQLite database\n# no-db: false\n\n# Disable daemon for RPC communication (forces direct database access)\n# no-daemon: false\n\n# Disable auto-flush of database to JSONL after mutations\n# no-auto-flush: false\n\n# Disable auto-import from JSONL when it's newer than database\n# no-auto-import: false\n\n# Enable JSON output by default\n# json: false\n\n# Default actor for audit trails (overridden by BD_ACTOR or --actor)\n# actor: \"\"\n\n# Path to database (overridden by BEADS_DB or --db)\n# db: \"\"\n\n# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON)\n# auto-start-daemon: true\n\n# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE)\n# flush-debounce: \"5s\"\n\n# Git branch for beads commits (bd sync will commit to this branch)\n# IMPORTANT: Set this for team projects so all clones use the same sync branch.\n# This setting persists across clones (unlike database config which is gitignored).\n# Can also use BEADS_SYNC_BRANCH env var for local override.\n# If not set, bd sync will require you to run 'bd config set sync.branch <branch>'.\n# sync-branch: \"beads-sync\"\n\n# Multi-repo configuration (experimental - bd-307)\n# Allows hydrating from multiple repositories and routing writes to the correct JSONL\n# repos:\n#   primary: \".\"  # Primary repo (where this database lives)\n#   additional:   # Additional repos to hydrate from (read-only)\n#     - ~/beads-planning  # Personal planning repo\n#     - ~/work-planning   # Work planning repo\n\n# Integration settings (access with 'bd config get/set')\n# These are stored in the database, not in this file:\n# - jira.url\n# - jira.project\n# - linear.url\n# - linear.api-key\n# - github.org\n# - github.repo\n"
  },
  {
    "path": ".beads/interactions.jsonl",
    "content": ""
  },
  {
    "path": ".beads/issues.jsonl",
    "content": "{\"id\":\"Grace-14w\",\"title\":\"P1 Add src/docs/ENVIRONMENT.md from EnvironmentVariables\",\"description\":\"Document env vars and dependencies (Docker services, auth forwarding) using Grace.Shared.EnvironmentVariables as source of truth.\",\"acceptance_criteria\":\"ENVIRONMENT.md matches code constants; markdownlint passes.\",\"notes\":\"Added src/docs/ENVIRONMENT.md documenting Docker dependencies, Aspire run modes, test toggles, auth forwarding, and all EnvironmentVariables entries.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:31:02.7535804-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:11:35.0366104-08:00\",\"closed_at\":\"2026-01-09T13:11:35.0366104-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-14w\",\"depends_on_id\":\"Grace-4ri\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:46:38.9582602-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-19m\",\"title\":\"Ensure CI runs Fantomas format check\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"bug\",\"created_at\":\"2026-01-09T23:05:13.2613072-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T23:12:18.120212-08:00\",\"closed_at\":\"2026-01-09T23:12:18.120212-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-2k6\",\"title\":\"CLI: Queue commands\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:55.0263408-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T04:52:05.2894182-08:00\",\"closed_at\":\"2026-01-06T04:52:05.2894182-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-3yl\",\"title\":\"P0 Bootstrap script: restore tools/packages + output\",\"description\":\"Extend scripts/bootstrap.ps1 to run dotnet tool restore and restore NuGet packages for chosen build/test targets; print next commands and elapsed time on success/failure.\",\"acceptance_criteria\":\"bootstrap.ps1 performs tool restore + package restore; prints next steps (validate -Fast) and elapsed time; exits 0 on success.\",\"notes\":\"bootstrap.ps1 restores tools and src/Grace.sln packages, prints next steps and elapsed time. Ran pwsh ./scripts/bootstrap.ps1 -SkipDocker -CI successfully.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:29:04.7741508-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:05:47.2243686-08:00\",\"closed_at\":\"2026-01-09T13:05:47.2243686-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-3yl\",\"depends_on_id\":\"Grace-ami\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:32:52.5983134-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-3yl\",\"depends_on_id\":\"Grace-4d0\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:32:57.929904-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-40p\",\"title\":\"P0 CI workflow: validate -Full on main/nightly\",\"description\":\"Extend validate workflow to run pwsh ./scripts/validate.ps1 -Full on main pushes and/or nightly schedule; ensure Docker available for Aspire tests.\",\"acceptance_criteria\":\"Full validation runs on main/nightly and fails if full suite fails; logs show stage failures.\",\"notes\":\"validate.yml runs -Full on main pushes and nightly schedule with docker info step.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:30:30.6850857-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:09:31.9266816-08:00\",\"closed_at\":\"2026-01-09T13:09:31.9266816-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-40p\",\"depends_on_id\":\"Grace-701\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:34:07.4312764-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-40p\",\"depends_on_id\":\"Grace-wv2\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:34:12.7693199-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-49s\",\"title\":\"CLI: WorkItem commands\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:54.5109757-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T04:03:39.3374957-08:00\",\"closed_at\":\"2026-01-06T04:03:39.3374957-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-4ba\",\"title\":\"P0 Define bootstrap/validate semantics + acceptance\",\"description\":\"Draft exact behavior for scripts (flags, working dir vs explicit paths, format check strategy, build/test targets) and failure/success outputs; include 'I know I'm done when…' checks.\",\"acceptance_criteria\":\"Issue contains concrete command list for bootstrap/validate, flag behavior, and acceptance checks for success/failure paths.\",\"notes\":\"Proposed semantics:\\\\nbootstrap.ps1\\\\n- StrictMode, Continue='Stop'.\\\\n- Flags: -SkipDocker, -CI (no prompts), -Verbose (PS standard).\\\\n- Checks: PowerShell 7.x (PSVersionTable.PSVersion.Major -ge 7), dotnet available (Get-Command dotnet), dotnet SDK resolves to net10 (dotnet --version + optionally dotnet --list-sdks; if global.json missing, warn), Docker running unless -SkipDocker (docker info).\\\\n- Actions: dotnet tool restore; dotnet restore src/Grace.sln (or explicit projects if needed).\\\\n- Output: grouped headings (Prereqs/Restore/Next steps), print elapsed time on success/failure. Exit 0 on success, non-zero otherwise.\\\\n\\\\nvalidate.ps1\\\\n- StrictMode, stop-on-error, grouped output: Format, Build, Test. Always prints elapsed time.\\\\n- Flags: -Fast (default), -Full, -SkipFormat/-SkipBuild/-SkipTests, -Configuration (default Release).\\\\n- Format stage: dotnet tool restore; run pinned Fantomas against src/ using src/fantomas-config.json. Prefer dotnet tool run fantomas --check src --config src/fantomas-config.json; if --check not reliable, run format then git status --porcelain diff and fail with guidance.\\\\n- Build stage: dotnet build src/Grace.Server/Grace.Server.fsproj -c \\u003cconfig\\u003e; dotnet build src/Grace.CLI/Grace.CLI.fsproj -c \\u003cconfig\\u003e; optional Grace.SDK depending on inventory.\\\\n- Test stage: -Fast runs dotnet test src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj -c \\u003cconfig\\u003e --no-build. -Full runs that plus dotnet test src/Grace.Server.Tests/Grace.Server.Tests.fsproj -c \\u003cconfig\\u003e --no-build (Aspire/Docker).\\\\n\\\\nAcceptance checks for scripts:\\\\n- bootstrap: missing docker =\\u003e fail with actionable message unless -SkipDocker; missing dotnet/pwsh =\\u003e fail; tool restore + package restore succeed; prints next steps.\\\\n- validate: any stage failure =\\u003e non-zero exit; formatting drift yields actionable guidance; -Fast avoids Docker; -Full exercises Aspire tests.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:28:32.6383989-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:33:02.276854-08:00\",\"closed_at\":\"2026-01-09T13:33:02.276854-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-4d0\",\"title\":\"P0 Add .config/dotnet-tools.json with pinned Fantomas\",\"description\":\"Create local tool manifest at repo root and pin fantomas-tool version; ensure validate uses dotnet tool run.\",\"acceptance_criteria\":\"dotnet tool restore succeeds at repo root; dotnet tool run fantomas --version matches pinned version.\",\"notes\":\"Added .config/dotnet-tools.json pinning fantomas-tool 4.7.9. dotnet tool restore + dotnet tool run fantomas --version succeeded.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:28:42.9941774-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T12:52:49.5079712-08:00\",\"closed_at\":\"2026-01-09T12:52:49.5079712-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-4d0\",\"depends_on_id\":\"Grace-w54\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:32:36.5957243-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-4g4\",\"title\":\"Fix verbose parse result option handling\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T01:34:19.1573701-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T01:38:22.0352754-08:00\",\"closed_at\":\"2026-01-09T01:38:22.0352754-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-4ri\",\"title\":\"P0 Inventory current build/test/format commands\",\"description\":\"Read AGENTS.md, src/AGENTS.md, src/docs/ASPIRE_SETUP.md, src/fantomas-config.json, src/.editorconfig; locate sln/projects/tests and existing scripts; identify fast vs full test candidates.\",\"acceptance_criteria\":\"Notes captured in issue: canonical build targets, fast tests, full tests, format config paths, any existing scripts/tools.\",\"notes\":\"Inventory summary:\\\\n- Solution: src/Grace.sln\\\\n- Projects: Grace.Server, Grace.CLI, Grace.SDK, Grace.Actors, Grace.Shared, Grace.Types, Grace.Load; tests: Grace.CLI.Tests, Grace.Server.Tests\\\\n- Format config: src/fantomas-config.json; .editorconfig lives under src/\\\\n- Current guidance: dotnet build --configuration Release; dotnet test --no-build; run fantomas . after F# changes (root + project AGENTS).\\\\n- Aspire: src/docs/ASPIRE_SETUP.md (Aspire app host in Grace.Aspire.AppHost; tests reuse running emulators)\\\\n- Grace.Server.Tests uses [\\u003cSetUpFixture\\u003e] in namespace Grace.Server.Tests and AspireTestHost.startAsync() which spins containers; first call must be POST /owner/create per src/Grace.Server.Tests/AGENTS.md.\\\\n- Fast tests candidate: src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj (non-Docker). Full tests: src/Grace.Server.Tests/Grace.Server.Tests.fsproj (Aspire).\\\\n- No scripts/ directory currently in repo root.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:28:20.9873729-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:32:51.8031394-08:00\",\"closed_at\":\"2026-01-09T13:32:51.8031394-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-4x5\",\"title\":\"Tests: policy parser/snapshot determinism\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:55.2846176-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T05:13:35.5451478-08:00\",\"closed_at\":\"2026-01-06T05:13:35.5451478-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-5c3\",\"title\":\"Authorization/Access control implementation\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2025-12-28T23:07:44.6284068-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T02:05:17.4202501-08:00\",\"closed_at\":\"2025-12-30T02:05:17.4202501-08:00\",\"close_reason\":\"All tasks completed\"}\n{\"id\":\"Grace-5c3.1\",\"title\":\"Authz: codebase reconnaissance and conventions\",\"description\":\"Review existing patterns (actors, handlers, params, SDK/CLI) and locate stubs/target endpoints to protect; confirm serializer and validation conventions.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:07:58.2365723-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:14:19.3631334-08:00\",\"closed_at\":\"2025-12-28T23:14:19.3631334-08:00\",\"close_reason\":\"Recon complete\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.1\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:07:58.2414666-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.10\",\"title\":\"Authz: unit + integration tests\",\"description\":\"Add pure authorization unit tests and server integration tests for /access, enforcement, and auth headers; update General.Server.Tests.fs headers.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:36.7155492-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-29T00:17:49.3242606-08:00\",\"closed_at\":\"2025-12-29T00:17:49.3242606-08:00\",\"close_reason\":\"Closed\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.10\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:36.7204264-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.11\",\"title\":\"Authz: formatting/build/test verification\",\"description\":\"Run fantomas, dotnet build Release, and dotnet test for Server.Tests + CLI.Tests after code changes.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:40.9300256-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-29T01:27:14.0420934-08:00\",\"closed_at\":\"2025-12-29T01:27:14.0420934-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.11\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:40.9347831-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.12\",\"title\":\"Auth: external auth config + claim mapping\",\"description\":\"Add auth provider config model + env var bindings; implement claims transformation mapping external provider claims to grace_user_id and grace_claim (MSA + Entra). Include provider-agnostic mapping helper + /auth/me endpoint + unit tests for mapping logic.\",\"notes\":\"Config + claim mapping + /auth/me + unit tests\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-30T00:20:35.5829516-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T01:08:29.3280438-08:00\",\"closed_at\":\"2025-12-30T01:08:29.3280438-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.12\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-30T00:20:35.5835408-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.13\",\"title\":\"Auth: login provider selection page + endpoints\",\"description\":\"Add /auth/login page listing configured providers; add /auth/login/{provider} challenge endpoint + returnUrl handling; add /auth/logout endpoint; wire routes in Startup.Server.fs; show only providers with valid config.\",\"notes\":\"Added /auth/login page, provider challenge route, and /auth/logout.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-30T00:20:37.3729975-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T01:10:58.7806669-08:00\",\"closed_at\":\"2025-12-30T01:10:58.7806669-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.13\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-30T00:20:37.3784688-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.14\",\"title\":\"Auth: Microsoft (MSA) OIDC + JWT bearer integration\",\"description\":\"Configure Cookie + OpenIdConnect (Microsoft) and JwtBearer schemes for MSA+Entra; keep GraceTest when GRACE_TESTING=1; set default challenge/forbid scheme selection and callbacks; validate JWT audience for Grace API scope.\",\"notes\":\"Configured auth schemes: cookie + OIDC + JWT with scheme selection; MSA/Entra authority and audience validation.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-30T00:20:39.1621794-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T01:13:51.2451983-08:00\",\"closed_at\":\"2025-12-30T01:13:51.2451983-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.14\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-30T00:20:39.1676651-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.15\",\"title\":\"Auth: auth pipeline tests\",\"description\":\"Add unit tests for claim mapping; integration test for /auth/login provider list + challenge route (no external IdP calls); verify GraceTest auth still works. Add minimal CLI auth tests if feasible without IdP calls.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-30T00:20:41.0655512-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T02:02:09.4835738-08:00\",\"closed_at\":\"2025-12-30T02:02:09.4835738-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.15\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-30T00:20:41.0709904-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.16\",\"title\":\"Docs: MSA app registration + local config\",\"description\":\"Document MSA+Entra app registration (web + CLI apps), API scope, redirect URIs, required env vars/user-secrets, and CLI login usage. Note GitHub/Google placeholders for future providers.\",\"status\":\"closed\",\"priority\":3,\"issue_type\":\"task\",\"created_at\":\"2025-12-30T00:20:42.9343128-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T02:03:24.8795061-08:00\",\"closed_at\":\"2025-12-30T02:03:24.8795061-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.16\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-30T00:20:42.9402113-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.17\",\"title\":\"Auth: CLI/GUI device-code login + token cache\",\"description\":\"Add CLI auth commands (login/status/logout/whoami) using MSAL device code; store tokens in OS-protected cache; update Grace SDK/CLI HTTP client to send Bearer tokens; update grace connect to require auth / prompt login; support re-auth in existing repos.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-30T01:00:58.2662025-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-30T01:52:08.0842766-08:00\",\"closed_at\":\"2025-12-30T01:52:08.0842766-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.17\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-30T01:00:58.2777028-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.2\",\"title\":\"Authz: add authorization domain types\",\"description\":\"Add Authorization.Types.fs in Grace.Types with Principal/Scope/Resource/Operation/Role types + update fsproj; follow serializer/versioning conventions.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:02.281657-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:16:34.3723615-08:00\",\"closed_at\":\"2025-12-28T23:16:34.3723615-08:00\",\"close_reason\":\"Authorization types added\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.2\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:02.2871518-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.3\",\"title\":\"Authz: implement pure authorization core\",\"description\":\"Add Authorization.Shared.fs in Grace.Shared with RoleCatalog, scope resolution, effective ops, path permission checks, and checkPermission.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:06.5102728-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:27:54.83891-08:00\",\"closed_at\":\"2025-12-28T23:27:54.83891-08:00\",\"close_reason\":\"Authorization core added\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.3\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:06.5158736-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.4\",\"title\":\"Authz: AccessControl actor + RepositoryPermission actor\",\"description\":\"Implement AccessControl.Actor.fs + interface + constants; complete RepositoryPermission.Actor.fs with upsert/remove/list path permissions.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:10.4735628-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:35:20.2381363-08:00\",\"closed_at\":\"2025-12-28T23:35:20.2381363-08:00\",\"close_reason\":\"AccessControl + RepositoryPermission actors added\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.4\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:10.4792076-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.5\",\"title\":\"Authz: server security plumbing\",\"description\":\"Add GraceTest auth handler, PrincipalMapper, and PermissionEvaluator service; wire DI/auth scheme selection in Startup.Server.fs.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:14.5953805-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:38:47.1199966-08:00\",\"closed_at\":\"2025-12-28T23:38:47.1199966-08:00\",\"close_reason\":\"Test auth + permission evaluator wired\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.5\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:14.6007802-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.6\",\"title\":\"Authz: /access parameters, handlers, routing\",\"description\":\"Add Access.Parameters.fs, Access.Server.fs handlers, and /access routes with metadata/auth requirements.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:19.0690592-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:45:37.0740752-08:00\",\"closed_at\":\"2025-12-28T23:45:37.0740752-08:00\",\"close_reason\":\"Access parameters and endpoints added\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.6\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:19.0750897-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.7\",\"title\":\"Authz: SDK access surface\",\"description\":\"Add Access.SDK.fs methods for /access endpoints and update Grace.SDK.fsproj.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:23.1901245-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-28T23:46:44.3452613-08:00\",\"closed_at\":\"2025-12-28T23:46:44.3452613-08:00\",\"close_reason\":\"Access SDK added\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.7\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:23.1949443-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.8\",\"title\":\"Authz: CLI access commands\",\"description\":\"Add Access.CLI.fs command tree + options and register in Program.CLI.fs; output supports --json.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:27.2244712-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-29T00:04:34.4116541-08:00\",\"closed_at\":\"2025-12-29T00:04:34.4116541-08:00\",\"close_reason\":\"Closed\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.8\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:27.2298486-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-5c3.9\",\"title\":\"Authz: enforce permissions on endpoints\",\"description\":\"Add requiresPermission middleware and protect at least repo-level + path-level endpoints (e.g., setDescription + getUploadMetadataForFiles).\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2025-12-28T23:08:32.0246786-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2025-12-29T00:11:24.16764-08:00\",\"closed_at\":\"2025-12-29T00:11:24.16764-08:00\",\"close_reason\":\"Closed\",\"dependencies\":[{\"issue_id\":\"Grace-5c3.9\",\"depends_on_id\":\"Grace-5c3\",\"type\":\"parent-child\",\"created_at\":\"2025-12-28T23:08:32.0309851-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-6ev\",\"title\":\"P0 Update root AGENTS.md with Agent Quickstart\",\"description\":\"Add Agent Quickstart (Local) section with bootstrap/validate commands and links to src/AGENTS.md, src/docs/ASPIRE_SETUP.md, src/docs/ENVIRONMENT.md.\",\"acceptance_criteria\":\"Root AGENTS.md enables a new contributor to reach validate -Fast without other docs; markdownlint passes.\",\"notes\":\"Added Agent Quickstart (Local) section with bootstrap/validate commands and links. Markdownlint clean.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:30:09.9470165-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:19:19.5266104-08:00\",\"closed_at\":\"2026-01-09T13:19:19.5266104-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-6ev\",\"depends_on_id\":\"Grace-3yl\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:40.6102889-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-6ev\",\"depends_on_id\":\"Grace-oos\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:46.0467868-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-6r8\",\"title\":\"Server: Gate endpoints\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:50.5517025-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:17:22.7194502-08:00\",\"closed_at\":\"2026-01-06T02:17:22.7194502-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-701\",\"title\":\"P0 Validate script: full tests (Aspire)\",\"description\":\"Implement -Full test stage to run Grace.Server.Tests (Aspire.Hosting.Testing) including Docker-required integration tests.\",\"acceptance_criteria\":\"validate -Full runs full suite including Grace.Server.Tests and fails if Aspire stack fails to start; stage output grouped under Test.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:29:59.7615959-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:33:14.0639429-08:00\",\"closed_at\":\"2026-01-09T13:33:14.0639429-08:00\",\"close_reason\":\"Closed\",\"dependencies\":[{\"issue_id\":\"Grace-701\",\"depends_on_id\":\"Grace-sae\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:35.2800434-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-783\",\"title\":\"Eventing: event envelopes + publishers\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:53.3113195-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:53:25.0597006-08:00\",\"closed_at\":\"2026-01-06T02:53:25.0597006-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-79p\",\"title\":\"Models: OpenRouter config + wiring\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:51.9340196-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:33:34.4239983-08:00\",\"closed_at\":\"2026-01-06T02:33:34.4239983-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-7it\",\"title\":\"Continuous Review + Refinery Epic\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"feature\",\"created_at\":\"2026-01-06T01:14:46.8131949-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T21:09:54.0007067-08:00\",\"closed_at\":\"2026-01-06T21:09:54.0007067-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-7mo\",\"title\":\"Types: Queue/Candidate/Gate contracts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:47.9931636-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:27:47.5686453-08:00\",\"closed_at\":\"2026-01-06T01:27:47.5686453-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-85r\",\"title\":\"PAT auth: tests, docs, and validation\",\"description\":\"Add tests, update AGENTS, run build/test/format gates.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2026-01-01T17:34:00.4855223-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T18:11:35.0366791-08:00\",\"closed_at\":\"2026-01-01T18:11:35.0366791-08:00\",\"close_reason\":\"Completed\"}\n{\"id\":\"Grace-85r.1\",\"title\":\"Add server PAT auth tests\",\"description\":\"Extend Grace.Server.Tests/Auth.Server.Tests.fs with PAT create/use/revoke/max lifetime/list coverage.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:50.2931526-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:56:41.3090062-08:00\",\"closed_at\":\"2026-01-01T17:56:41.3090062-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-85r.1\",\"depends_on_id\":\"Grace-85r\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:50.3177996-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-85r.2\",\"title\":\"Add CLI token precedence tests\",\"description\":\"Extend Grace.CLI.Tests/Auth.Tests.fs with GRACE_TOKEN and token file precedence coverage.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:55.1234788-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:57:17.3295681-08:00\",\"closed_at\":\"2026-01-01T17:57:17.3295681-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-85r.2\",\"depends_on_id\":\"Grace-85r\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:55.1790724-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-85r.3\",\"title\":\"Update AGENTS docs and run validation gates\",\"description\":\"Update relevant AGENTS.md notes, run fantomas, dotnet build, and dotnet test; record any gaps.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:36:01.8844532-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T18:11:28.367927-08:00\",\"closed_at\":\"2026-01-01T18:11:28.367927-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-85r.3\",\"depends_on_id\":\"Grace-85r\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:36:01.8892664-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-87d\",\"title\":\"Server: Policy endpoints\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:49.8561522-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:53:19.2119153-08:00\",\"closed_at\":\"2026-01-06T01:53:19.2119153-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-8da\",\"title\":\"P1 Update src/AGENTS.md to reference bootstrap/validate\",\"description\":\"If needed, add canonical script commands to src/AGENTS.md and point to root quickstart.\",\"acceptance_criteria\":\"src/AGENTS.md references scripts/bootstrap.ps1 and scripts/validate.ps1 where appropriate; markdownlint passes.\",\"notes\":\"Added Local Commands section referencing bootstrap/validate; reflowed content to satisfy markdownlint.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:30:41.0594165-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:19:24.8779756-08:00\",\"closed_at\":\"2026-01-09T13:19:24.8779756-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-8da\",\"depends_on_id\":\"Grace-6ev\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:46:28.3087657-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-8fr\",\"title\":\"PAT auth: shared constants, parameters, and types\",\"description\":\"Add shared env vars, auth parameters, and PersonalAccessToken domain types + helpers.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2026-01-01T17:33:27.2157893-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:54:43.4429551-08:00\",\"closed_at\":\"2026-01-01T17:54:43.4429551-08:00\",\"close_reason\":\"Completed\"}\n{\"id\":\"Grace-8fr.1\",\"title\":\"Add PAT env var constants\",\"description\":\"Add GRACE_TOKEN/GRACE_TOKEN_FILE and PAT lifetime policy env var constants in Grace.Shared.Constants.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:05.5857926-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:37:44.6561645-08:00\",\"closed_at\":\"2026-01-01T17:37:44.6561645-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-8fr.1\",\"depends_on_id\":\"Grace-8fr\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:05.5912383-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-8fr.2\",\"title\":\"Add auth parameter classes\",\"description\":\"Create Grace.Shared/Parameters/Auth.Parameters.fs with Create/List/Revoke PAT parameter classes and update Grace.Shared.fsproj compile order.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:16.8985265-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:38:31.1388783-08:00\",\"closed_at\":\"2026-01-01T17:38:31.1388783-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-8fr.2\",\"depends_on_id\":\"Grace-8fr\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:16.9034524-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-8fr.3\",\"title\":\"Add PersonalAccessToken domain types\",\"description\":\"Add Grace.Types/PersonalAccessToken.Types.fs with DTOs, token format helpers, and update Grace.Types.fsproj compile order.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:24.0570351-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:39:18.6577737-08:00\",\"closed_at\":\"2026-01-01T17:39:18.6577737-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-8fr.3\",\"depends_on_id\":\"Grace-8fr\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:24.0624638-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-8j4\",\"title\":\"Actors: Review actor\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:48.9321457-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:39:31.9797897-08:00\",\"closed_at\":\"2026-01-06T01:39:31.9797897-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-8o8\",\"title\":\"P0 Validate script: Fantomas format check\",\"description\":\"Implement formatting stage using pinned Fantomas via dotnet tool run, targeting src/ with src/fantomas-config.json; fail if changes would be made.\",\"acceptance_criteria\":\"validate -Fast fails on formatting drift with actionable message; no-modify check or diff detection works reliably.\",\"notes\":\"Format stage uses pinned Fantomas and checks changed F# files under src (skips when none).\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:29:26.7841738-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:06:10.1257545-08:00\",\"closed_at\":\"2026-01-09T13:06:10.1257545-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-8o8\",\"depends_on_id\":\"Grace-oos\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:13.9405047-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-8o8\",\"depends_on_id\":\"Grace-4d0\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:19.2764964-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-9io\",\"title\":\"Add connect repo shortcut\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-08T17:09:36.3620353-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-08T17:09:46.9519324-08:00\"}\n{\"id\":\"Grace-9rp\",\"title\":\"Review: baseline drift semantics\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:51.4673919-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:31:25.1215525-08:00\",\"closed_at\":\"2026-01-06T02:31:25.1215525-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-ami\",\"title\":\"P0 Bootstrap script: prerequisite checks + flags\",\"description\":\"Create scripts/bootstrap.ps1 with strict mode, -SkipDocker/-CI/-Verbose flags; check PowerShell 7.x, dotnet SDK 10, and docker (unless -SkipDocker). Fail fast with actionable messages.\",\"acceptance_criteria\":\"bootstrap.ps1 exits non-zero on missing prereqs with clear guidance; -SkipDocker bypasses docker check.\",\"notes\":\"Created scripts/bootstrap.ps1 with strict mode and prerequisite checks (pwsh, dotnet 10, docker unless -SkipDocker).\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:28:54.3384334-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:05:41.8877551-08:00\",\"closed_at\":\"2026-01-09T13:05:41.8877551-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-ami\",\"depends_on_id\":\"Grace-4ri\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:32:41.9330047-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-ami\",\"depends_on_id\":\"Grace-4ba\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:32:47.2558435-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-an2\",\"title\":\"Tests: Stage 0 determinism\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:55.5616099-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T05:13:35.668343-08:00\",\"closed_at\":\"2026-01-06T05:13:35.668343-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-awb\",\"title\":\"Actors: Policy actor\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:48.710556-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:36:16.117935-08:00\",\"closed_at\":\"2026-01-06T01:36:16.117935-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-aya\",\"title\":\"SDK: Review APIs\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:54.0043743-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:56:31.2425971-08:00\",\"closed_at\":\"2026-01-06T02:56:31.2425971-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-b8s\",\"title\":\"Use git diff for CI format targets\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"bug\",\"created_at\":\"2026-01-09T23:28:07.3073561-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T23:56:01.7720601-08:00\",\"closed_at\":\"2026-01-09T23:56:01.7720601-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-ba0\",\"title\":\"Models: deep pipeline + progressive retrieval\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:52.3927211-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:36:34.5699307-08:00\",\"closed_at\":\"2026-01-06T02:36:34.5699307-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-cca\",\"title\":\"Review: deterministic chaptering + packet assembly\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:51.2457243-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:30:30.9647007-08:00\",\"closed_at\":\"2026-01-06T02:30:30.9647007-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-chq\",\"title\":\"Server: Review endpoints\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:50.0836908-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:56:02.1303505-08:00\",\"closed_at\":\"2026-01-06T01:56:02.1303505-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-d6y\",\"title\":\"Queue: gate framework + attestations\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:52.8430355-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:46:56.9159633-08:00\",\"closed_at\":\"2026-01-06T02:46:56.9159633-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-dcs\",\"title\":\"Fix build errors in Grace.CLI and Grace.Server\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-10T12:54:09.3683809-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-10T12:57:33.2944729-08:00\",\"closed_at\":\"2026-01-10T12:57:33.2944729-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-dof\",\"title\":\"CLI fallback to server OIDC config endpoint\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-08T01:49:50.4603822-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-08T02:03:33.5007944-08:00\",\"closed_at\":\"2026-01-08T02:03:33.5007944-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-dv3\",\"title\":\"Show resolved implicit ids in verbose parse output\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T00:34:41.3147349-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T00:34:41.3147349-08:00\"}\n{\"id\":\"Grace-e10\",\"title\":\"P2 Optional: CODEOWNERS + security scans\",\"description\":\"Add CODEOWNERS and basic security scan config if desired; document required reviewers.\",\"acceptance_criteria\":\"Guardrails added and documented; minimal overhead.\",\"status\":\"open\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:32:04.8295523-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T12:32:04.8295523-08:00\"}\n{\"id\":\"Grace-e28\",\"title\":\"Models: provider abstraction\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:51.6936304-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:33:27.2168749-08:00\",\"closed_at\":\"2026-01-06T02:33:27.2168749-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-ed9\",\"title\":\"SDK: Policy APIs\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:53.7805499-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:56:24.5163565-08:00\",\"closed_at\":\"2026-01-06T02:56:24.5163565-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-er5\",\"title\":\"Types: Stage 0 + Evidence contracts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:47.5344426-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:24:19.0517267-08:00\",\"closed_at\":\"2026-01-06T01:24:19.0517267-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-f1u\",\"title\":\"PAT auth: SDK support\",\"description\":\"Add SDK wrappers and server URI handling for PAT endpoints.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2026-01-01T17:33:44.9629598-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:49:56.514499-08:00\",\"closed_at\":\"2026-01-01T17:49:56.514499-08:00\",\"close_reason\":\"Completed\"}\n{\"id\":\"Grace-f1u.1\",\"title\":\"Add PAT SDK wrappers\",\"description\":\"Implement Grace.SDK PersonalAccessToken wrappers for create/list/revoke and update project compile list.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:17.2437547-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:49:41.0114696-08:00\",\"closed_at\":\"2026-01-01T17:49:41.0114696-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-f1u.1\",\"depends_on_id\":\"Grace-f1u\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:17.2831975-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-f1u.2\",\"title\":\"Ensure PAT SDK uses GRACE_SERVER_URI\",\"description\":\"Make PAT SDK endpoints work without graceconfig.json by using GRACE_SERVER_URI or a helper that bypasses Configuration.Current().\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:24.0343531-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:49:49.2757481-08:00\",\"closed_at\":\"2026-01-01T17:49:49.2757481-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-f1u.2\",\"depends_on_id\":\"Grace-f1u\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:24.0786964-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-f2t\",\"title\":\"Server: derived computation trigger consumer\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:49.3862902-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:44:59.6212182-08:00\",\"closed_at\":\"2026-01-06T01:44:59.6212182-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-far\",\"title\":\"Allow grace connect without config when parse errors occur\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"bug\",\"created_at\":\"2026-01-09T02:38:47.1058823-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T02:40:29.8344153-08:00\",\"closed_at\":\"2026-01-09T02:40:29.8344153-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-g11\",\"title\":\"SDK: Queue/Candidate APIs\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:54.2524092-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:56:38.0685742-08:00\",\"closed_at\":\"2026-01-06T02:56:38.0685742-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-g4d\",\"title\":\"Types: Work Item contracts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:47.0471393-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:20:50.4717585-08:00\",\"closed_at\":\"2026-01-06T01:20:50.4717585-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-hj3\",\"title\":\"Types: RequiredAction + Event envelope contracts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:48.2364827-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:29:04.1667329-08:00\",\"closed_at\":\"2026-01-06T01:29:04.1667329-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-i2i\",\"title\":\"Evidence: distiller selection/budgets/redaction\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:51.0086084-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:28:55.4077656-08:00\",\"closed_at\":\"2026-01-06T02:28:55.4077656-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-iqx\",\"title\":\"P2 Optional: minimal analyzers baseline\",\"description\":\"Add minimal analyzers policy/baseline without large churn; keep incremental.\",\"acceptance_criteria\":\"Analyzers add signal with minimal new warnings; documented.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:31:55.4502453-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T22:07:24.2198936-08:00\",\"closed_at\":\"2026-01-09T22:07:24.2198936-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-k62\",\"title\":\"Actors: PromotionQueue/Runner actor\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:49.1640588-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:42:29.7695364-08:00\",\"closed_at\":\"2026-01-06T01:42:29.7695364-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-kto\",\"title\":\"Models: triage pipeline + caching + receipts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:52.166834-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:35:35.5622322-08:00\",\"closed_at\":\"2026-01-06T02:35:35.5622322-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-lha\",\"title\":\"Actors: WorkItem actor\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:48.4777306-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:33:35.0831146-08:00\",\"closed_at\":\"2026-01-06T01:33:35.0831146-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-lp2\",\"title\":\"Defer CLI defaults for help\",\"description\":\"Implement deferred defaults + help customization to avoid config access during help. See spec in chat.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-02T21:13:38.8410648-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-02T21:52:40.9246985-08:00\",\"closed_at\":\"2026-01-02T21:52:40.9246985-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-lvj\",\"title\":\"Server: WorkItem endpoints\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:49.6281939-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:50:44.0992215-08:00\",\"closed_at\":\"2026-01-06T01:50:44.0992215-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-mnz\",\"title\":\"Queue: IntegrationCandidate state + required-actions computation\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:52.6213732-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:42:01.2885484-08:00\",\"closed_at\":\"2026-01-06T02:42:01.2885484-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-nk3\",\"title\":\"Tests: chaptering + findings lifecycle\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:56.089585-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T05:13:35.9094287-08:00\",\"closed_at\":\"2026-01-06T05:13:35.9094287-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-np3\",\"title\":\"P1 Add Smoke tests (Aspire host + /healthz)\",\"description\":\"Add 1–3 smoke tests that start Aspire test host and verify /healthz succeeds; tag Category('Smoke') and avoid heavy SetUpFixture if possible.\",\"acceptance_criteria\":\"Smoke tests fail when server doesn't boot or /healthz unreachable; stable on known-good branch.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:31:24.4441191-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:33:25.1169846-08:00\",\"closed_at\":\"2026-01-09T13:33:25.1169846-08:00\",\"close_reason\":\"Closed\",\"dependencies\":[{\"issue_id\":\"Grace-np3\",\"depends_on_id\":\"Grace-4ri\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:46:49.6036483-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-o3z\",\"title\":\"PAT auth: Orleans actor storage\",\"description\":\"Add PersonalAccessToken grain interface, state, proxy, and implementation.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2026-01-01T17:33:33.121442-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:42:57.8202313-08:00\",\"closed_at\":\"2026-01-01T17:42:57.8202313-08:00\",\"close_reason\":\"Completed\"}\n{\"id\":\"Grace-o3z.1\",\"title\":\"Add PAT actor constants and interface\",\"description\":\"Add PersonalAccessToken actor/state names and IPersonalAccessTokenActor interface in Grace.Actors.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:29.5312209-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:40:21.9276448-08:00\",\"closed_at\":\"2026-01-01T17:40:21.9276448-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-o3z.1\",\"depends_on_id\":\"Grace-o3z\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:29.5383697-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-o3z.2\",\"title\":\"Add PAT actor proxy helper\",\"description\":\"Add ActorProxy.PersonalAccessToken.CreateActorProxy helper in Grace.Actors/ActorProxy.Extensions.Actor.fs.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:36.1715625-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:40:54.6647026-08:00\",\"closed_at\":\"2026-01-01T17:40:54.6647026-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-o3z.2\",\"depends_on_id\":\"Grace-o3z\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:36.1768734-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-o3z.3\",\"title\":\"Implement PersonalAccessToken actor\",\"description\":\"Create PersonalAccessToken.Actor.fs with state, create/list/revoke/validate logic, and update Grace.Actors.fsproj compile list.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:42.8177451-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:42:50.9637143-08:00\",\"closed_at\":\"2026-01-01T17:42:50.9637143-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-o3z.3\",\"depends_on_id\":\"Grace-o3z\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:42.8272088-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-o9j\",\"title\":\"CLI: Review commands\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:54.7685857-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T04:47:46.728238-08:00\",\"closed_at\":\"2026-01-06T04:47:46.728238-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-onc\",\"title\":\"Stage 0: deterministic analysis + storage\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:50.7848498-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:26:05.9658068-08:00\",\"closed_at\":\"2026-01-06T02:26:05.9658068-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-oos\",\"title\":\"P0 Validate script: scaffold + flags + timing\",\"description\":\"Create scripts/validate.ps1 with strict mode, -Fast default, -Full, -SkipFormat/-SkipBuild/-SkipTests, -Configuration; grouped output and elapsed-time reporting on all exits.\",\"acceptance_criteria\":\"validate.ps1 parses flags correctly, groups output by stage, and always prints elapsed time; exits non-zero on any enabled stage failure.\",\"notes\":\"Added scripts/validate.ps1 with flags (-Fast default, -Full, skip flags, -Configuration), strict mode, grouped output, elapsed time.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:29:16.4509589-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:06:04.7979977-08:00\",\"closed_at\":\"2026-01-09T13:06:04.7979977-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-oos\",\"depends_on_id\":\"Grace-4ri\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:03.2601336-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-oos\",\"depends_on_id\":\"Grace-4ba\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:08.5802437-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-pdu\",\"title\":\"Types: Policy snapshot contracts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:47.2979787-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:22:35.5838892-08:00\",\"closed_at\":\"2026-01-06T01:22:35.5838892-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-qxx\",\"title\":\"Investigate dotnet test Aspire shutdown crash\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"bug\",\"created_at\":\"2026-01-09T22:51:43.9281309-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T23:01:15.4475995-08:00\",\"closed_at\":\"2026-01-09T23:01:15.4475995-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-ra9\",\"title\":\"P0 Validate script: build fast targets\",\"description\":\"Implement build stage for -Fast/-Full using minimal top-level projects (Grace.Server, Grace.CLI, optional Grace.SDK) discovered in inventory.\",\"acceptance_criteria\":\"validate -Fast builds selected targets with configuration flag; build stage clearly reported.\",\"notes\":\"Build stage runs dotnet build for Grace.Server, Grace.CLI, Grace.SDK with configuration flag.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:29:36.9439517-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:06:15.4581959-08:00\",\"closed_at\":\"2026-01-09T13:06:15.4581959-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-ra9\",\"depends_on_id\":\"Grace-oos\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:24.6185628-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-rai\",\"title\":\"SDK: WorkItem APIs\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:53.535762-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:56:17.5684417-08:00\",\"closed_at\":\"2026-01-06T02:56:17.5684417-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-rro\",\"title\":\"Types: Review contracts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:47.7663008-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T01:26:35.7331201-08:00\",\"closed_at\":\"2026-01-06T01:26:35.7331201-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-ry8\",\"title\":\"Tests: queue runner + gates + conflict pipeline\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:56.3670092-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T05:13:36.0290699-08:00\",\"closed_at\":\"2026-01-06T05:13:36.0290699-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-s2p\",\"title\":\"Queue: conflict pipeline + resolution receipts\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:53.0840653-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:50:54.6547631-08:00\",\"closed_at\":\"2026-01-06T02:50:54.6547631-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-sae\",\"title\":\"P0 Validate script: fast tests (non-Docker)\",\"description\":\"Implement fast test stage for -Fast (e.g., Grace.CLI.Tests) excluding Aspire/Docker tests; ensure suitable for tight iteration.\",\"acceptance_criteria\":\"validate -Fast runs only non-Docker tests and succeeds on known-good branch; stage output grouped under Test.\",\"notes\":\"Fast tests run Grace.CLI.Tests --no-build. validate -Fast (with -SkipFormat) succeeded; format stage now stable.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:29:48.5479559-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:06:20.7942067-08:00\",\"closed_at\":\"2026-01-09T13:06:20.7942067-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-sae\",\"depends_on_id\":\"Grace-ra9\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:29.9523268-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-suw\",\"title\":\"P1 Update ASPIRE_SETUP.md to link bootstrap/validate\",\"description\":\"Add note that bootstrap/validate are the preferred local entrypoints; link to root quickstart.\",\"acceptance_criteria\":\"ASPIRE_SETUP.md references scripts; markdownlint passes.\",\"notes\":\"Updated ASPIRE_SETUP with preferred scripts and reflowed content; markdownlint clean.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:30:51.4307308-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:19:30.221513-08:00\",\"closed_at\":\"2026-01-09T13:19:30.221513-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-suw\",\"depends_on_id\":\"Grace-6ev\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:46:33.6471259-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-ts5\",\"title\":\"P1 Add slop tests for fragile invariants\",\"description\":\"Add a few targeted tests (contract headers/status, DTO serialization round-trip, invariants) that fail on plausible wrong edits; include comment describing failure scenario.\",\"acceptance_criteria\":\"At least 3 slop tests added; each guards a plausible incorrect change; stable in full suite.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:31:34.7872318-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:33:36.9645044-08:00\",\"closed_at\":\"2026-01-09T13:33:36.9645044-08:00\",\"close_reason\":\"Closed\",\"dependencies\":[{\"issue_id\":\"Grace-ts5\",\"depends_on_id\":\"Grace-np3\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:46:54.9127456-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-vsv\",\"title\":\"PAT auth: CLI commands and token precedence\",\"description\":\"Implement CLI PAT commands, local token storage, and auth precedence.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2026-01-01T17:33:51.2305524-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:54:37.6298734-08:00\",\"closed_at\":\"2026-01-01T17:54:37.6298734-08:00\",\"close_reason\":\"Completed\"}\n{\"id\":\"Grace-vsv.1\",\"title\":\"Add token source precedence and local storage\",\"description\":\"Update Auth.CLI token resolution (env/file/MSAL), add token file helpers, and strip optional Bearer prefix.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:30.7597332-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:50:41.9317473-08:00\",\"closed_at\":\"2026-01-01T17:50:41.9317473-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-vsv.1\",\"depends_on_id\":\"Grace-vsv\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:30.764604-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-vsv.2\",\"title\":\"Implement auth token create/list/revoke commands\",\"description\":\"Add CLI commands for PAT create/list/revoke, duration parsing, and local token cleanup on revoke.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:36.9800155-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:53:15.3707676-08:00\",\"closed_at\":\"2026-01-01T17:53:15.3707676-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-vsv.2\",\"depends_on_id\":\"Grace-vsv\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:36.9922134-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-vsv.3\",\"title\":\"Implement auth token set/clear/status commands\",\"description\":\"Add CLI commands to set PAT from arg/stdin, clear local token file, and show credential source status without secrets.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:43.4007282-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:54:29.5165138-08:00\",\"closed_at\":\"2026-01-01T17:54:29.5165138-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-vsv.3\",\"depends_on_id\":\"Grace-vsv\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:43.4136176-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-vz3\",\"title\":\"Include principal in auth failure logs\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-08T16:50:06.2590869-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-08T16:51:04.6921304-08:00\",\"closed_at\":\"2026-01-08T16:51:04.6921304-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-w54\",\"title\":\"A1 Add global.json to pin .NET 10 SDK\",\"description\":\"Add root global.json pinning .NET 10 SDK with rollForward policy; document if prerelease required.\",\"acceptance_criteria\":\"dotnet --version resolves to pinned SDK (or allowed roll-forward) in repo; dotnet build uses pinned SDK without unexpected preview warnings.\",\"notes\":\"Added root global.json with sdk 10.0.100 + rollForward latestPatch. dotnet --version resolves to 10.0.101 in repo (roll-forward OK).\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:27:41.3775109-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T12:51:32.5042882-08:00\",\"closed_at\":\"2026-01-09T12:51:32.5042882-08:00\"}\n{\"id\":\"Grace-wr1\",\"title\":\"PAT auth: server auth and endpoints\",\"description\":\"Add PAT auth handler, policy enforcement, endpoints, routing, and log redaction.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"epic\",\"created_at\":\"2026-01-01T17:33:39.5652826-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:48:27.2865385-08:00\",\"closed_at\":\"2026-01-01T17:48:27.2865385-08:00\",\"close_reason\":\"Completed\"}\n{\"id\":\"Grace-wr1.1\",\"title\":\"Add PAT auth handler\",\"description\":\"Implement PersonalAccessTokenAuthHandler and add server compile include.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:49.7829-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:44:26.0693244-08:00\",\"closed_at\":\"2026-01-01T17:44:26.0693244-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-wr1.1\",\"depends_on_id\":\"Grace-wr1\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:49.8374394-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-wr1.2\",\"title\":\"Wire PAT auth schemes and routing\",\"description\":\"Update Startup.Server.fs to route Bearer PATs to GracePat scheme in both testing and non-testing branches.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:34:56.4070495-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:46:10.5846197-08:00\",\"closed_at\":\"2026-01-01T17:46:10.5846197-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-wr1.2\",\"depends_on_id\":\"Grace-wr1\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:34:56.4678879-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-wr1.3\",\"title\":\"Add PAT auth endpoints and policy enforcement\",\"description\":\"Implement /auth/token create/list/revoke handlers with lifetime policy enforcement and route wiring.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:03.9243801-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:47:51.5135409-08:00\",\"closed_at\":\"2026-01-01T17:47:51.5135409-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-wr1.3\",\"depends_on_id\":\"Grace-wr1\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:03.9356681-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-wr1.4\",\"title\":\"Redact auth headers in request logging\",\"description\":\"Update LogRequestHeaders middleware to redact Authorization/Cookie and token-like headers.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-01T17:35:10.8898229-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:48:21.3517527-08:00\",\"closed_at\":\"2026-01-01T17:48:21.3517527-08:00\",\"close_reason\":\"Completed\",\"dependencies\":[{\"issue_id\":\"Grace-wr1.4\",\"depends_on_id\":\"Grace-wr1\",\"type\":\"parent-child\",\"created_at\":\"2026-01-01T17:35:10.899608-08:00\",\"created_by\":\"daemon\"}]}\n{\"id\":\"Grace-wv2\",\"title\":\"P0 CI workflow: validate -Fast on PR\",\"description\":\"Add .github/workflows/validate.yml to run pwsh ./scripts/validate.ps1 -Fast on pull_request using actions/setup-dotnet and pinned SDK.\",\"acceptance_criteria\":\"PR workflow runs validate -Fast and fails on any stage; output shows which stage failed.\",\"notes\":\"Added .github/workflows/validate.yml to run pwsh ./scripts/validate.ps1 -Fast on PRs using global.json and Aspire workload.\",\"status\":\"closed\",\"priority\":0,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:30:20.4736489-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:09:26.6012157-08:00\",\"closed_at\":\"2026-01-09T13:09:26.6012157-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-wv2\",\"depends_on_id\":\"Grace-8o8\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:51.411236-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-wv2\",\"depends_on_id\":\"Grace-ra9\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:33:56.7406619-08:00\",\"created_by\":\"unknown\"},{\"issue_id\":\"Grace-wv2\",\"depends_on_id\":\"Grace-sae\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:34:02.0817118-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-xdn\",\"title\":\"P2 Optional: scripts/install-githooks.ps1\",\"description\":\"Add opt-in githook installer to run validate -Fast pre-commit without conflicting with bd hooks install.\",\"acceptance_criteria\":\"Hook installer is reversible/opt-in and documented.\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:31:45.6807321-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T22:07:11.9277259-08:00\",\"closed_at\":\"2026-01-09T22:07:11.9277259-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-xjz\",\"title\":\"Server: Queue/Candidate endpoints\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:50.3187454-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T02:06:26.5016007-08:00\",\"closed_at\":\"2026-01-06T02:06:26.5016007-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-yal\",\"title\":\"Tests: evidence selection determinism\",\"status\":\"closed\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-06T01:14:55.8237777-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-06T05:13:35.7871715-08:00\",\"closed_at\":\"2026-01-06T05:13:35.7871715-08:00\",\"close_reason\":\"Closed\"}\n{\"id\":\"Grace-ymb\",\"title\":\"P1 Add .env.example with safe placeholders\",\"description\":\"Add .env.example at repo root with placeholders for common env vars (telemetry/auth/testing toggles) without secrets.\",\"acceptance_criteria\":\".env.example contains placeholders only; no secrets; aligns with ENVIRONMENT.md.\",\"notes\":\"Added .env.example with safe placeholder values for common variables and test toggles.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"task\",\"created_at\":\"2026-01-09T12:31:12.161231-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-09T13:11:40.3783357-08:00\",\"closed_at\":\"2026-01-09T13:11:40.3783357-08:00\",\"dependencies\":[{\"issue_id\":\"Grace-ymb\",\"depends_on_id\":\"Grace-14w\",\"type\":\"blocks\",\"created_at\":\"2026-01-09T12:46:44.2810471-08:00\",\"created_by\":\"unknown\"}]}\n{\"id\":\"Grace-ymj\",\"title\":\"Add server-mediated Microsoft/Entra device login for CLI\",\"description\":\"Introduce server-mediated Microsoft/Entra CLI login flow so users don't need auth env vars; CLI obtains device code/session from server, polls for completion, stores Grace token.\",\"status\":\"closed\",\"priority\":1,\"issue_type\":\"feature\",\"created_at\":\"2025-12-30T23:33:48.6726147-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-01T17:33:19.7525387-08:00\",\"closed_at\":\"2026-01-01T17:33:19.7525387-08:00\",\"close_reason\":\"Superseded by multi-epic PAT auth plan\"}\n{\"id\":\"Grace-zzi\",\"title\":\"Diagnose grace watch notifications not receiving checkpoint events\",\"status\":\"in_progress\",\"priority\":2,\"issue_type\":\"task\",\"created_at\":\"2026-01-08T23:41:49.5127036-08:00\",\"created_by\":\"Scott Arbeit\",\"updated_at\":\"2026-01-08T23:42:00.1809879-08:00\"}\n"
  },
  {
    "path": ".beads/metadata.json",
    "content": "{\n  \"database\": \"beads.db\",\n  \"jsonl_export\": \"issues.jsonl\",\n  \"last_bd_version\": \"0.40.0\"\n}"
  },
  {
    "path": ".config/dotnet-tools.json",
    "content": "{\n  \"version\": 1,\n  \"isRoot\": true,\n  \"tools\": {\n    \"fantomas-tool\": {\n      \"version\": \"4.7.9\",\n      \"commands\": [\n        \"fantomas\"\n      ]\n    }\n  }\n}\r\n"
  },
  {
    "path": ".gitattributes",
    "content": "\n# Use bd merge for beads JSONL files\n.beads/issues.jsonl merge=beads\n"
  },
  {
    "path": ".github/code_review_instructions.md",
    "content": "# Grace Code Review instructions\n\nThis file is intended to be the guidance for GitHub Copilot to perform automated code reviews in Grace PR's.\n\n## Code Review instructions\n\n_note: for now, these instructions are in no particular order, I'm just capturing ideas._\n\n### F# Standards\n\nRules in this section deal with our use of F# in the codebase, and specify stylistic and syntactical choices we mnight make.\n\n#### Always use `task { }`; never use `async { }` for asynchronous code.\n\nOlder versions of F# used the `async { }` computation expression to write asynchonous code. In fact, the C# `async/await` syntax, which has since been adopted by TypeScript and JavaScript, was inspired by `async { }` in F#.\n\nThe `task { }` computation expression for asynchonous code was added in F# 6.0. `task { }` uses the same stuff from `System.Threading.Tasks` that C# does for `async/await` code, allowing it to take advantage of all of the performance improvements in Tasks that each version of .NET delivers.\n\nAny use of `async { }` in Grace should be considered an error and should be rewritten using `task { }`.\n\n### Internal consistency\n\nRules in this section are intended to enforce consistent use of utilities and constructs provided by Grace.\n\n#### All grain references must have the CorrelationId set using RequestContext.Set().\n\nThe actors expect to be able to call `RequestContext.Get(Constants.CorrelationId)` to get the CorrelationId when they need it, and so the grain client is responsible for setting the CorrelationId on it by calling `RequestContext.Set(Constants.CorrelationId, <some correlationId>)`.\n\nThe easiest way to do this is to use the ActorProxy extensions in ActorProxy.Extensions.Actor.fs. Each of those helper methods - `Branch.CreateActorProxy`, `Repository.CreateActorProxy`, etc. - takes `correlationId` as a parameter. They each set the CorrelationId for you in the RequestContext.\n\nIf any grain references are created by calling `IGrainFactory.GetGrain<'TGrainInterface>(key)` without using the ActorProxy extensions, they should be closely inspected to ensure that `RequestContext.Set(Constants.CorrelationId, <some correlationId>)` is called before any use of the grain reference to call a grain interface method. The recommendation is to rewrite the code to use the ActorProxy extensions to eliminate any possibility of an error.\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Copilot Operating Instructions for Grace\n\n## Load Repository Guidance\n\n-   Always begin by reviewing `src/agents.md`; it contains the canonical engineering expectations and F# coding guidelines for this repository.\n-   When working inside a specific project (for example `Grace.Actors`, `Grace.Server`, or `Grace.SDK`), consult the matching `AGENTS.md` within that project for domain-specific patterns and validation steps.\n-   Use the information in those `AGENTS.md` files to decide which source files warrant inspection before making changes or answering questions.\n\n## Response & Communication Style\n\n-   Present information formally while keeping the direct interaction with the user conversational.\n-   Provide thorough, well-structured answers to questions; keep code or text-generation tasks concise and implementation-focused.\n-   Assume the user has a post-graduate education and deep programming experience.\n\n## Coding & Tooling Expectations\n\n-   Default to F# for all programming discussions unless the user states otherwise, and summarise the resulting code in English.\n-   Follow the F# guidelines described in `src/agents.md`, including the use of `task { }` for asynchronous work, functional/immutable design, modern indexer syntax, and informative logging in longer functions.\n-   Generate complete, copy/paste-ready F# snippets with appropriate inline comments when the logic is non-trivial.\n-   Produce PowerShell for scripting tasks unless directed to use another shell or language.\n\n## Task Support & Planning\n\n-   When the user asks for help organising work, assume inattentive-type ADHD: supply concrete, bite-sized steps suitable for a Kanban board and offer CBT-informed prompts to initiate progress.\n\n## Azure-Specific Rule\n\n-   @azure Rule – Use Azure Best Practices: Before generating Azure-related code, commands, or plans, invoke `azure_development-get_best_practices` when available.\n\n- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools.\n- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first.\n- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it.\n"
  },
  {
    "path": ".github/workflows/dotnet.yml",
    "content": "name: .NET Restore / Build / Test\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  build:\n    #runs-on: arc-runner-set\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Setup .NET\n      uses: actions/setup-dotnet@v4\n      with:\n        dotnet-version: 10.0.x\n    - name: Install .NET Aspire workload\n      run: dotnet workload install aspire\n    - name: Restore dependencies\n      run: dotnet restore Grace.slnx\n      working-directory: src\n    - name: Build\n      run: dotnet build Grace.slnx --no-restore -c Release\n      working-directory: src\n#    - name: Test\n#      run: dotnet test Grace.sln --no-build --verbosity normal\n#      working-directory: src\n"
  },
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: Validate\n\non:\n  pull_request:\n    branches: [ main ]\n  push:\n    branches: [ main ]\n  schedule:\n    - cron: \"0 3 * * *\"\n\njobs:\n  fast:\n    if: github.event_name == 'pull_request'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          global-json-file: global.json\n      - name: Install .NET Aspire workload\n        run: dotnet workload install aspire\n      - name: Validate (Fast)\n        run: pwsh ./scripts/validate.ps1 -Fast\n\n  full:\n    if: github.event_name != 'pull_request'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          global-json-file: global.json\n      - name: Install .NET Aspire workload\n        run: dotnet workload install aspire\n      - name: Docker info\n        run: docker info\n      - name: Pre-pull Aspire images\n        run: |\n          docker pull redis:latest\n          docker pull mcr.microsoft.com/azure-storage/azurite:latest\n          docker pull mcr.microsoft.com/mssql/server:2022-latest\n          docker pull mcr.microsoft.com/azure-messaging/servicebus-emulator:latest\n          docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest\n      - name: Validate (Full)\n        run: pwsh ./scripts/validate.ps1 -Full\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\r\n## files generated by popular Visual Studio add-ons.\r\n\r\n# Grace-specific ignores\n.grace/\n**/.grace/objects\n**/.grace/directoryVersions\n**/.grace/gracestatus*\n/src/Grace.Server/logs/\n**/*.backup\r\n**/appsettings.Development.json\r\n/Play/\r\n/.vscode/\r\n**/[Cc]omponents\r\n**/dapr\r\n**/*[Aa]zurite*\r\n**/.env\r\n*CodexPlan.*\r\n\r\n# Secrets file to configure Orleans persistence\r\nOrleans.secrets.fs\r\n\r\n# Visual Studio (>=2015) project-specific, machine local files\r\n.vs/\r\n\r\n# User-specific files\r\n*.suo\r\n*.user\r\n*.sln.docstates\r\n*.userprefs\r\n\r\n# ignore Xamarin.Android Resource.Designer.cs files\r\n**/*.Droid/**/[Rr]esource.[Dd]esigner.cs\r\n**/*.Android/**/[Rr]esource.[Dd]esigner.cs\r\n**/Android/**/[Rr]esource.[Dd]esigner.cs\r\n**/Droid/**/[Rr]esource.[Dd]esigner.cs\r\n\r\n# Xamarin Components\r\nComponents/\r\n\r\n# Build results\r\n[Bb]in/\r\n[Oo]bj/\r\n[Dd]ebug/\r\n[Dd]ebugPublic/\r\n[Rr]elease/\r\nx64/\r\nbuild/\r\nbld/\r\n\r\n# MSTest test Results\r\n[Tt]est[Rr]esult*/\r\n[Bb]uild[Ll]og.*\r\n\r\n#NUNIT\r\n*.VisualState.xml\r\nTestResult.xml\r\n\r\n# Build Results of an ATL Project\r\n[Dd]ebugPS/\r\n[Rr]eleasePS/\r\ndlldata.c\r\n\r\n*_i.c\r\n*_p.c\r\n*_i.h\r\n*.ilk\r\n*.meta\r\n*.obj\r\n*.pch\r\n*.pdb\r\n*.pgc\r\n*.pgd\r\n*.rsp\r\n*.sbr\r\n*.tlb\r\n*.tli\r\n*.tlh\r\n*.tmp\r\n*.tmp_proj\r\n*.log\r\n*.vspscc\r\n*.vssscc\r\n.builds\r\n*.pidb\r\n*.svclog\r\n*.scc\r\n\r\n# Chutzpah Test files\r\n_Chutzpah*\r\n\r\n# Visual C++ cache files\r\nipch/\r\n*.aps\r\n*.ncb\r\n*.opensdf\r\n*.sdf\r\n*.cachefile\r\n\r\n# Visual Studio profiler\r\n*.psess\r\n*.vsp\r\n*.vspx\r\n\r\n# TFS 2012 Local Workspace\r\n$tf/\r\n\r\n# Guidance Automation Toolkit\r\n*.gpState\r\n\r\n# ReSharper is a .NET coding add-in\r\n_ReSharper*/\r\n*.[Rr]e[Ss]harper\r\n*.DotSettings.user\r\n\r\n# JustCode is a .NET coding addin-in\r\n.JustCode\r\n\r\n# TeamCity is a build add-in\r\n_TeamCity*\r\n\r\n# DotCover is a Code Coverage Tool\r\n*.dotCover\r\n\r\n# NCrunch\r\n*.ncrunch*\r\n_NCrunch_*\r\n.*crunch*.local.xml\r\n\r\n# MightyMoose\r\n*.mm.*\r\nAutoTest.Net/\r\n\r\n# Web workbench (sass)\r\n.sass-cache/\r\n\r\n# Installshield output folder\r\n[Ee]xpress/\r\n\r\n# DocProject is a documentation generator add-in\r\nDocProject/buildhelp/\r\nDocProject/Help/*.HxT\r\nDocProject/Help/*.HxC\r\nDocProject/Help/*.hhc\r\nDocProject/Help/*.hhk\r\nDocProject/Help/*.hhp\r\nDocProject/Help/Html2\r\nDocProject/Help/html\r\n\r\n# Click-Once directory\r\npublish/\r\n\r\n# Publish Web Output\r\n*.[Pp]ublish.xml\r\n*.azurePubxml\r\n\r\n# NuGet Packages Directory\r\npackages/\r\n*.nuget.targets\r\n*.lock.json\r\n*.nuget.props\r\n\r\n## TODO: If the tool you use requires repositories.config uncomment the next line\r\n#!packages/repositories.config\r\n\r\n# Enable \"build/\" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets\r\n# This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented)\r\n!packages/build/\r\n\r\n# Windows Azure Build Output\r\ncsx/\r\n*.build.csdef\r\n\r\n# Windows Store app package directory\r\nAppPackages/\r\n\r\n# Others\r\nsql/\r\n*.[Cc]ache\r\nClientBin/\r\n[Ss]tyle[Cc]op.*\r\n~$*\r\n*~\r\n*.dbmdl\r\n*.dbproj.schemaview\r\n*.pfx\r\n*.publishsettings\r\nnode_modules/\r\n.DS_Store\r\n*.bak\r\n\r\n# RIA/Silverlight projects\r\nGenerated_Code/\r\n\r\n# Backup & report files from converting an old project file to a newer\r\n# Visual Studio version. Backup files are not needed, because we have git ;-)\r\n_UpgradeReport_Files/\r\nBackup*/\r\nUpgradeLog*.XML\r\nUpgradeLog*.htm\r\n\r\n# SQL Server files\r\n*.mdf\r\n*.ldf\r\n\r\n# Business Intelligence projects\r\n*.rdl.data\r\n*.bim.layout\r\n*.bim_*.settings\r\n\r\n# Microsoft Fakes\r\nFakesAssemblies/\r\n.vscode/settings.json\r\n.fake\r\n.ionide\r\n/src/OpenAPI/Grace.OpenAPI.html\r\n/src/OpenAPI/GenerateOpenAPI.ps1\r\n/src/Check-CosmosDB-RUs.ps1\r\n*.nettrace\r\n*.gcdump\r\n\r\n/plans\r\n"
  },
  {
    "path": ".markdownlint.jsonc",
    "content": "{\n  // Global markdownlint configuration for this repo.\n  \"MD013\": {\n    \"line_length\": 120\n  }\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Agent Instructions\n\nOther `AGENTS.md` files exist in subdirectories, refer to them for more specific context.\n\n## Agent Quickstart (Local)\n\nPrerequisites:\n\n- PowerShell 7.x\n- .NET 10 SDK\n- Docker Desktop (required for `-Full`)\n\nCommands:\n\n- `pwsh ./scripts/bootstrap.ps1`\n- `pwsh ./scripts/validate.ps1 -Fast`\n\nUse `pwsh ./scripts/validate.ps1 -Full` for Aspire integration coverage.\nOptional: `pwsh ./scripts/install-githooks.ps1` to add a pre-commit `validate -Fast` hook.\n\nMore context:\n\n- `src/AGENTS.md`\n- `src/docs/ASPIRE_SETUP.md`\n- `src/docs/ENVIRONMENT.md`\n\n## Issue Tracking\n\nDo not use `bd`/beads workflows in this repository unless a maintainer explicitly asks for it in the current task.\nUse the plan/log files requested in the task (for example `CodexPlan.md`) plus normal git commits instead.\n\n## Markdown Guidelines\n\n- Follow the MarkdownLint ruleset found at `https://raw.githubusercontent.com/DavidAnson/markdownlint/refs/heads/main/doc/Rules.md`.\n- Verify updates by running MarkdownLint. Use `npx --yes markdownlint-cli2 ...`. `--help` is available.\n- For MD013, override the guidance to allow for 120-character lines.\n\n## Editing Documentation\n\nWhen updating documentation files, follow these guidelines:\n\n- When writing technical documentation, act as a friendly peer engineer helping other developers to understand Grace as a project.\n- When writing product-focused documentation, act as an expert product manager who helps a tech-aware audience understand Grace as a product, and helps end users understand how to use Grace effectively.\n- Use clear, concise language; avoid jargon. The tone should be welcoming and informative.\n- Structure content with headings and subheadings. Intersperse written (paragraph / sentence form) documentation with bullet points for readability.\n- Keep documentation up to date with code changes; review related docs when modifying functionality. Explain all documentation changes clearly, both what is changing, and why it's changing.\n- Show all scripting examples in both (first) PowerShell and (then, second) bash/zsh, where applicable. bash and zsh are always spelled in lowercase.\n\nPowerShell:\n```powershell\n$env:GRACE_SERVER_URI=\"http://localhost:5000\"\n```\n\nbash / zsh:\n```bash\nexport GRACE_SERVER_URI=\"http://localhost:5000\"\n```\r\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Grace Version Control System\r\n\r\nThanks for considering a contribution to **Grace Version Control System**.\r\n\r\nThis repo is primarily **F#** and targets **.NET 10**.\r\n\r\n## Quick start\r\n\r\n1. Fork the repo and create your branch in your fork.\r\n2. Run the prerequisite check:\r\n\r\n   - From the repo root:\r\n     - `pwsh ./scripts/bootstrap.ps1`\r\n       - Optional: `-SkipDocker` if you don’t have Docker available yet.\r\n\r\n3. Build:\r\n\r\n   - `dotnet build ./src/Grace.sln`\r\n\r\n4. Test:\r\n\r\n   - `dotnet test ./src/Grace.sln` (optionally add `-c Release`)\r\n   - Or run the repo validator: `pwsh ./scripts/validate.ps1 -Full`\r\n\r\n5. Format F#:\r\n\r\n   - From `./src`: `dotnet tool run fantomas --recurse .`\r\n\r\n## Contribution workflow\r\n\r\n- Use the **GitHub fork + pull request** workflow.\r\n- Keep PRs focused (one change per PR whenever practical).\r\n- Add or update tests when changing behavior.\r\n- Please add any useful AI prompts you used for diagnosis or implementation to the PR description.\r\n\r\n## Prerequisites\r\n\r\n- **.NET 10 SDK** (see: https://dotnet.microsoft.com/download)\r\n- **PowerShell 7+** (see: https://learn.microsoft.com/powershell/)\r\n- **Docker Desktop** or **Podman** (recommended for local emulators / Aspire DebugLocal)\r\n  - Windows/macOS: https://www.docker.com/products/docker-desktop/\r\n  - Linux: use Docker Engine for your distro\r\n\r\n`pwsh ./scripts/bootstrap.ps1` is the recommended sanity check. It verifies tools and performs `dotnet tool restore` + `dotnet restore`.\r\n\r\n## Build\r\n\r\nFrom the repo root:\r\n\r\n- `dotnet build ./src/Grace.sln`\r\n\r\n## Tests\r\n\r\nFrom the repo root:\r\n\r\n- `dotnet test ./src/Grace.sln` (optionally `-c Release`)\r\n\r\nThis repo also includes a validation script:\r\n\r\n- Fast loop: `pwsh ./scripts/validate.ps1 -Fast`\r\n- Full validation (includes Aspire integration coverage): `pwsh ./scripts/validate.ps1 -Full`\r\n\r\n## Formatting (required)\r\n\r\nF# is formatted with **Fantomas**.\r\n\r\nFrom `./src`:\r\n\r\n- Apply formatting: `dotnet tool run fantomas --recurse .`\r\n\r\nIf you’re proposing CI changes, CI should enforce formatting checks (if it doesn’t already).\r\n\r\nFantomas:\r\n\r\n- https://github.com/fsprojects/fantomas\r\n\r\n## Running Grace locally (Aspire)\r\n\r\nGrace can be run locally using **Docker containers and emulators**, via the Aspire AppHost launch configuration:\r\n\r\n- `DebugLocal`: local containers/emulators (e.g., Azurite, Cosmos emulator, Service Bus emulator)\r\n- `DebugAzure`: runs locally but expects real Azure resources (Cosmos DB, Blob Storage, Service Bus, etc.)\r\n\r\nWhere to look in the repo:\r\n\r\n- `Grace.Aspire.AppHost/Properties/launchSettings.json`\r\n- `Grace.Aspire.AppHost/Program.Aspire.AppHost.cs`\r\n\r\nAspire:\r\n\r\n- https://learn.microsoft.com/dotnet/aspire/\r\n\r\n## Configuration and secrets\r\n\r\nGrace supports multiple configuration sources (depending on your setup):\r\n\r\n- Environment variables\r\n- `.NET user-secrets` (recommended for local dev)\r\n- `appsettings*.json` (project-specific)\r\n- Aspire launch profiles (`DebugLocal` / `DebugAzure`)\r\n- Azure resource configuration (when using `DebugAzure`)\r\n\r\n### Using .NET user-secrets (recommended)\r\n\r\nThe simplest developer setup is to store secrets in **user-secrets** for the `Grace.Server` project.\r\n\r\nUser-secrets documentation:\r\n\r\n- https://learn.microsoft.com/aspnet/core/security/app-secrets\r\n\r\nExamples (run from the repo root):\r\n\r\n- List secrets:\r\n  - `dotnet user-secrets list --project ./src/Grace.Server/Grace.Server.fsproj`\r\n\r\n- Set a secret:\r\n  - `dotnet user-secrets set --project ./src/Grace.Server/Grace.Server.fsproj \"grace__azurecosmosdb__connectionstring\" \"<value>\"`\r\n\r\n- Remove a secret:\r\n  - `dotnet user-secrets remove --project ./src/Grace.Server/Grace.Server.fsproj \"grace__azurecosmosdb__connectionstring\"`\r\n\r\nNotes:\r\n\r\n- Keys frequently use `__` to map to hierarchical configuration (see: https://learn.microsoft.com/aspnet/core/fundamentals/configuration/)\r\n- The Aspire AppHost can read the server user-secrets id and forward selected auth settings (see `Grace.Aspire.AppHost/AGENTS.md`).\r\n\r\n## Environment variables\r\n\r\nThe canonical list of environment variables is defined in `Grace.Shared/Constants.Shared.fs` under `Constants.EnvironmentVariables`.\r\n\r\nIn general, values may come from:\r\n\r\n- your shell environment,\r\n- Aspire launch profile environment,\r\n- `.NET user-secrets` for `Grace.Server`,\r\n- or Azure resources (when using `DebugAzure`).\r\n\r\n### Client variables\r\n\r\n- `GRACE_SERVER_URI`\r\n  - Grace server base URI.\r\n  - Must include port.\r\n  - Must not include a trailing slash.\r\n  - Example: `http://localhost:5000`\r\n\r\n- `GRACE_TOKEN`\r\n  - Personal access token for non-interactive auth.\r\n\r\n- `GRACE_TOKEN_FILE`\r\n  - Overrides the local token file path.\r\n\r\n### Telemetry\r\n\r\n- `grace__applicationinsightsconnectionstring`\r\n  - Application Insights connection string.\r\n  - Source: user-secrets/env/Azure App Insights resource.\r\n\r\n### Azure Cosmos DB\r\n\r\n- `grace__azurecosmosdb__connectionstring`\r\n  - Cosmos DB connection string.\r\n  - Source: user-secrets/env/Azure Cosmos DB.\r\n\r\n- `grace__azurecosmosdb__endpoint`\r\n  - Cosmos DB endpoint for managed identity scenarios.\r\n  - Source: Azure Cosmos DB account endpoint.\r\n\r\n- `grace__azurecosmosdb__database_name`\r\n  - Cosmos database name.\r\n\r\n- `grace__azurecosmosdb__container_name`\r\n  - Cosmos container name.\r\n\r\n### Azure Storage (Blob)\r\n\r\n- `grace__azure_storage__connectionstring`\r\n  - Storage connection string.\r\n\r\n- `grace__azure_storage__account_name`\r\n  - Overrides storage account name (managed identity).\r\n\r\n- `grace__azure_storage__endpoint_suffix`\r\n  - Storage endpoint suffix (default: `core.windows.net`).\r\n\r\n- `grace__azure_storage__key`\r\n  - Storage account key.\r\n\r\n- `grace__azure_storage__directoryversion_container_name`\r\n  - Container name for DirectoryVersions.\r\n\r\n- `grace__azure_storage__diff_container_name`\r\n  - Container name for cached diffs.\r\n\r\n- `grace__azure_storage__zipfile_container_name`\r\n  - Container name for zip files.\r\n\r\n### Azure Service Bus\r\n\r\n- `grace__azure_service_bus__connectionstring`\r\n  - Service Bus connection string.\r\n\r\n- `grace__azure_service_bus__namespace`\r\n  - Fully qualified namespace (for example `sb://<name>.servicebus.windows.net`).\r\n\r\n- `grace__azure_service_bus__topic`\r\n  - Topic name for Grace events.\r\n\r\n- `grace__azure_service_bus__subscription`\r\n  - Subscription name for Grace events.\r\n\r\n### Pub/Sub provider selection\r\n\r\n- `grace__pubsub__system`\r\n  - Selects the pub-sub provider implementation.\r\n\r\n### Orleans\r\n\r\n- `grace__orleans__clusterid`\r\n  - Orleans cluster id.\r\n\r\n- `grace__orleans__serviceid`\r\n  - Orleans service id.\r\n\r\nOrleans:\r\n- https://learn.microsoft.com/dotnet/orleans/\r\n\r\n### Redis\r\n\r\n- `grace__redis__host`\r\n  - Redis host.\r\n\r\n- `grace__redis__port`\r\n  - Redis port.\r\n\r\nRedis:\r\n- https://redis.io/docs/latest/\r\n\r\n### Auth (OIDC)\r\n\r\nThese are used for external auth providers such as Auth0 / OIDC.\r\n\r\n- `grace__auth__oidc__authority`\r\n- `grace__auth__oidc__audience`\r\n- `grace__auth__oidc__cli_client_id`\r\n- `grace__auth__oidc__cli_redirect_port`\r\n- `grace__auth__oidc__cli_scopes`\r\n- `grace__auth__oidc__m2m_client_id`\r\n- `grace__auth__oidc__m2m_client_secret`\r\n- `grace__auth__oidc__m2m_scopes`\r\n\r\nOpenID Connect:\r\n- https://openid.net/developers/how-connect-works/\r\n\r\n### Auth (deprecated Microsoft auth)\r\n\r\nThese constants are marked deprecated in code:\r\n\r\n- `grace__auth__microsoft__client_id`\r\n- `grace__auth__microsoft__client_secret`\r\n- `grace__auth__microsoft__tenant_id`\r\n- `grace__auth__microsoft__authority`\r\n- `grace__auth__microsoft__api_scope`\r\n- `grace__auth__microsoft__cli_client_id`\r\n\r\n### PAT policy\r\n\r\n- `grace__auth__pat__default_lifetime_days`\r\n- `grace__auth__pat__max_lifetime_days`\r\n- `grace__auth__pat__allow_no_expiry`\r\n\r\n### Authorization bootstrap\r\n\r\n- `grace__authz__bootstrap__system_admin_users`\r\n  - Semicolon-delimited list of user principals to bootstrap as SystemAdmin.\r\n\r\n- `grace__authz__bootstrap__system_admin_groups`\r\n  - Semicolon-delimited list of group principals to bootstrap as SystemAdmin.\r\n\r\n### Reminders and metrics\r\n\r\n- `grace__reminder__batch__size`\r\n  - Batch size for reminder retrieval/publish.\r\n\r\n- `grace__metrics__allow_anonymous`\r\n  - Allows anonymous access to Prometheus scraping endpoint.\r\n\r\nPrometheus:\r\n\r\n- https://prometheus.io/docs/introduction/overview/\r\n\r\n### Other providers (placeholders / future)\r\n\r\n- `grace__aws_sqs__queue_url`\r\n- `grace__aws_sqs__region`\r\n- `grace__gcp__projectid`\r\n- `grace__gcp__topic`\r\n- `grace__gcp__subscription`\r\n\r\n### Debugging/logging\r\n\r\n- `grace__debug_environment`\r\n  - Debug environment flag.\r\n\r\n- `grace__log_directory`\r\n  - Directory for Grace Server log files.\r\n\r\n## Pull request checklist\r\n\r\n- [ ] Builds: `dotnet build ./src/Grace.sln`\r\n- [ ] Tests: `dotnet test ./src/Grace.sln`\r\n- [ ] Formatting: run `dotnet tool run fantomas --recurse .` from `./src`\r\n- [ ] Documentation updated (if behavior changed)\r\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2022 Scott Arbeit\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Grace - Version Control for the AI Era\r\n\r\n<!-- markdownlint-disable MD013 -->\r\n\r\ngrace _(n)_ -\r\n\r\n1. elegance and beauty of movement, form, or expression\r\n2. a pleasing or charming quality\r\n3. goodwill or favor\r\n4. a sense of propriety and consideration for others [^grace]\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\nGrace is a **version control system** designed and built for the AI Era.\r\n\r\nGrace is designed for kindness to humans, and velocity for agents. It's meant to help you stay calm, in-control, and in-flow as you ship code at agentic speed.\r\n\r\nIt has all of the basics you'd expect from a version control system, plus primitives that help you monitor your agents, capture the work they're doing, and review that work in real-time using both deterministic checks and AI prompts that you can write, with rules that you set.\r\n\r\nGrace assumes that AI belongs in the version control system, reacting to events, reviewing changes, and generating whatever you need to get code from idea to agent to production.\r\n\r\nAlong with version control, Grace is designed to capture the work items, the prompts, the specifications, the model versions, and so much more - everything that goes into doing agentic coding - so you and your agents have the best possible context to complete the work successfully, and review it with confidence.\r\n\r\nAll you have to do is tell your agents to run `grace agent bootstrap` at the start of the session. Grace and your agents will take care of the rest, automatically capturing what's going on. The `bootstrap` is customizable, and Grace will even help you customize it.\r\n\r\nIn the repository, you define the checks you want run in respose to which events, you define the prompts, you choose the models, and Grace Server will do the rest, capturing the results in detailed Review reports. Again, Grace will help you define and customize all of it.\r\n\r\nOf course it's fast. Grace is multitenant, built for massive repositories, insane numbers of developers and agents, and ludicrous amounts of code and binary files of any size.\r\n\r\nIt ships with a promotion queue - Grace doesn't do merges, it does promotions - and automatically handles promotion conflict resolution according to rules and confidence levels you set.\r\n\r\n> ⚠️👷🏻🚧 Grace is an alpha, and is going through rapid evolution with breaking changes, but it's ready for feedback and contributions. It is not ready for or intended for production usage at this time.\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## Technology stack\r\n\r\nGrace is a modern, fast, powerful centralized version control system. It's made up of a web API, with a CLI (and soon a GUI).\r\n\r\nGrace is written primarily in **F#**, and uses:\r\n\r\n- **ASP.NET Core** for the HTTP API\r\n- **Orleans** for the virtual-actor and distributed-systems core\r\n- **Microsoft Azure PaaS services**[^1]:\r\n  - **Azure Cosmos DB** for actor state storage (repos, branches, references, directory versions, etc.) at ludicrous scale and speed\r\n  - **Azure Blob Storage** for objects and artifacts, including (virtually) unlimited-size binary files\r\n  - **Azure Service Bus** for event streams that you can hook into\r\n  - All come with emulators for frictionless local development\r\n- **SignalR** for live client-server coordination (`grace watch`)\r\n- **Redis** (used by SignalR and for caching)\r\n- **Aspire** to orchestrate everything for both local dev and cloud deployment\r\n- **Avalonia** (I think) for a fully cross-platform GUI, including WASM\r\n\r\n[^1]: Grace is designed to be adaptable to AWS and other cloud providers, and with coding agents, it should be not easy but not too hard to do. I just haven't done it yet.\r\n\r\n## Running Grace locally\r\n\r\nThe fastest way to understand Grace is to run it locally and poke at it.\r\n\r\nGrace is designed as a multitenant, massively scalable, centralized, cloud-native version control system, so running it locally isn't quite as simple as \"download this one executable and run it\". Grace uses [Aspire](https://aspire.dev) to make running Grace as simple as possible, with configurations for using either local emulators or actual Azure services.\r\n\r\n### Normal development and debugging\r\n\r\nIn normal development and debugging, there are three steps to running Grace.\r\n\r\n1. Run the Aspire AppHost project. The AppHost will start the local emulators (if requested), and run Grace Server.\r\n2. Use the Grace CLI to do Grace stuff.\r\n3. Stop the Aspire AppHost. The AppHost will stop the local emulators and Grace Server as it exits.\r\n\r\n### First time setup\r\n\r\nThe first-time steps below use **local emulators** and **test authentication** (i.e. the same authentication we use in integration tests), so you don't have to set anything up in the cloud to get started. If all goes well, you should be up and running in under 10 minutes.\r\n\r\n> There is a detailed guide to configuring authentication at [`/docs/Authentication.md`](/docs/Authentication.md).\r\n\r\n1. **Install prerequisites** for your platform:\r\n   - [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download)\r\n   - [PowerShell 7+](https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell)\r\n   - A container runtime\r\n     - Aspire supports [Docker Desktop](https://www.docker.com/get-started/) and [Podman](https://podman-desktop.io/).\r\n     - [Rancher](https://www.rancher.com/products/rancher-desktop) is not officially supported, but with Moby/dockerd it should work.\r\n\r\n    NOTE: If using Podman, set the [container runtime](https://aspire.dev/app-host/configuration/#common-configuration) to `podman`:\r\n\r\n    PowerShell:\r\n    ```powershell\r\n    $env:ASPIRE_CONTAINER_RUNTIME=\"podman\"\r\n    ```\r\n\r\n    bash/zsh:\r\n    ```bash\r\n    export ASPIRE_CONTAINER_RUNTIME=podman\r\n    ```\r\n\r\n2. **Clone the Grace repo**: `git clone https://github.com/ScottArbeit/Grace.git` or `gh repo clone ScottArbeit/Grace`.\r\n3. **Build the solution**: `dotnet build ./src/Grace.slnx` to sanity-check your environment.\r\n4. **Create an alias to make your life easier**: Add an alias to your profile called `grace` that points to `./src/Grace.CLI/bin/Debug/net10.0/grace.exe`.\r\n\r\n   PowerShell:\r\n\r\n   ```powershell\r\n   Set-Alias -Name grace -Value \\<repo-path\\>\\src\\Grace.CLI\\bin\\Debug\\net10.0\\grace.exe\r\n   ```\r\n\r\n   bash / zsh:\r\n\r\n   ```bash\r\n   alias grace=\"\\<repo-path\\>/src/Grace.CLI/bin/Debug/net10.0/grace.exe\"\r\n   ```\r\n\r\n5. **Choose a test repository**: You can create an empty directory to start with a blank repo in, or you can copy or clone some code into a directory to start with that code.\r\n\r\n6. **Start Grace Server**: Run `pwsh ./scripts/dev-local.ps1` to start Grace Server using Aspire. This will automatically generate a personal access token that you'll use for authentication.\r\n\r\n    When `dev-local.ps1` finishes, it will output your new token, along with exact copy/paste commands to set `GRACE_SERVER_URI` and `GRACE_TOKEN`, the first environment variables you'll need.\r\n\r\n7. **Create Owner, Organization, and Repository**: Copy and paste (or modify if you want) the scripts below to set up your first Grace repo.\r\n\r\n    ```powershell\r\n    # Create an owner, organization, and repo\r\n    grace owner create --owner-name demo\r\n    grace organization create --owner-name demo --organization-name sandbox\r\n    grace repository create --owner-name demo --organization-name sandbox --repository-name hello\r\n\r\n    # Connect to it (writes local Grace config for this working directory)\r\n    grace connect demo/sandbox/hello\r\n\r\n    # Initialize the repo with the contents of the current directory\r\n    grace repository init --directory .\r\n    ```\r\n\r\n    To see your current state:\r\n\r\n    ```powershell\r\n    grace status\r\n    ```\r\n\r\n    To see what's changed in your branch vs. previous states, use `grace diff`:\r\n\r\n    ```powershell\r\n    grace diff commit\r\n    grace diff promotion\r\n    grace diff checkpoint\r\n    ```\r\n\r\n### Verify the CLI can talk to the server\r\n\r\nOnce you have `GRACE_SERVER_URI` and `GRACE_TOKEN` set, run:\r\n\r\n```powershell\r\ngrace auth whoami\r\n```\r\n\r\n### File an issue if anything seems confusing or rough\r\n\r\nThe intention is for this first-time setup to be as easy as possible. If you run into any problems, please file an issue so we can make it smoother.\r\n\r\n### Running Grace and Git in the same directory\r\n\r\nYou can use Git and Grace side-by-side in the same directory. You just have to make sure they ignore each other's object directories, `.git` and `.grace`.\r\n\r\n#### Tell Git to ignore Grace\r\n\r\n`.grace` is Grace's version of the `.git` directory.\r\n\r\nTo ignore it, add the path `.grace/` to your `.gitignore`.\r\n\r\n#### Tell Grace to ignore Git\r\n\r\nGrace's `.graceignore` file, by default, ignores the `.git` directory.\r\n\r\nIf it happens to be missing, add the path `/.git/*` to `.graceignore` in the root of your repository.\r\n\r\n> Again, ⚠️👷🏻🚧 Grace is an alpha, and still has some alpha-like bugs. For now, I recommend testing Grace either on 1) repos that you won't be sad if something bad happens, or; 2) repos where you're comfortable running `git reset` to restore to a known-good version if you need it.\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## Architecture\r\n\r\n### 1) Grace Server is a modern web API that uses an actor system\r\n\r\nGrace is built on **Orleans**. Most domain behavior is implemented as virtual actors, which makes it effortless and natural to scale up and scale out.\r\n\r\n- HTTP API: `src/Grace.Server`\r\n- Actor implementations: `src/Grace.Actors`\r\n- Domain types and events: `src/Grace.Types`\r\n- Shared utilities and DTOs: `src/Grace.Shared`\r\n\r\n### 2) Grace is event-sourced\r\n\r\nA version control system is \\<waves hands\\> just a series of modifications to files and branches and repositories over time. Grace stores every modification to every entity as an event, as the source of truth. Grace then uses those events to pre-compute and cache projections that help you and your agents go faster.\r\n\r\n### 3) Files are stored in object storage\r\n\r\nGrace relies on cloud object storage systems to provide a safe and infinitely scalable storage layer. Grace currently uses Azure Blob Storage, with the intention of adding the ability to run it on AWS S3 and others. Currently, all files are stored as single blobs in Azure; soon Grace will shift to a content-addressible storage construct that will enable efficient handling of changes to large binary files.\r\n\r\n### 4) `grace watch` for effortless, background update tracking\r\n\r\n`grace watch` scans the working directory when it starts to get current state and notice any changes that happened while it wasn't running, and then continuously watches for changes, saving new file and directory versions to Grace Server, all generally within a second of the file being saved. This background processing saves time in every session, as other Grace commands like `grace commit` detect `grace watch` and can skip the costly directory rescans and other work that `grace watch` has already taken care of.\r\n\r\nGrace has other commands that agents are meant to use to capture the complete context of the work being done.\r\n\r\nTogether, they give us a step-by-step audit trail of everything an agent has done and is doing, and why, that you can watch and review in real-time from your computer.\r\n\r\n> If you want a deeper dive on what `grace watch` does and does not do, see: [What `grace watch` does](./docs/What%20grace%20watch%20does.md).\r\n\r\n### 5) “Continuous review” for rapid validation of changes\r\n\r\nGrace’s review system is designed to bring AI evaluations and human review and approvals together in one harmonious flow.\r\n\r\nKey concepts include:\r\n\r\n- **Policy snapshots** (immutable rule bundles)\r\n- **Stage 0 analysis** (deterministic signals recorded for references)\r\n- **Promotion queues** and **integration candidates**\r\n- **Gates** and **attestations**\r\n- **Review packets** with easy-to-understand, customizable summaries\r\n\r\nFor more, see: `docs/Continuous review.md`\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## Roadmap\r\n\r\nGrace is evolving quickly. Strap in....\r\n\r\n### GUI\r\n\r\n- Native GUI for Windows, MacOS, Linux Desktop, Android, iOS, and WASM.\r\n- Not Electron.\r\n\r\n### Full rewrite of object storage layer\r\n\r\n- Switch to content-addressable storage for more efficient object storage and network transfer\r\n- Automatic chunking of large binary files\r\n\r\n### Multi-hash semantics\r\n\r\n- Add abstractions to support multiple hashing algorithms at once\r\n- Ensure that Grace is not locked into just one hashing algorithm\r\n- Add BLAKE3 hashing for better performance and to support content-addressable storage\r\n\r\n### `grace cache` for CI/CD and in-office scenarios\r\n\r\n- Built-in feature of Grace CLI\r\n- Like a Git mirror, plus full authentication and authorization\r\n- Pre-fetch by listening to repository events using SignalR and downloading the branches and versions you want\r\n- Transparent read-through cache for CI workers and in-office users to save time and bandwidth\r\n\r\n### Improved database backpressure handling\r\n\r\n### Agent skills to help you create and update Grace's automatic review policies\r\n\r\n### `grace agent bootstrap` command to give coding agents context for using Grace\r\n\r\n### Ergonomics\r\n\r\n- smoother onboarding\r\n- better defaults\r\n\r\n### Repeatable performance benchmarking\r\n\r\n### Even more unit tests\r\n\r\n### Even more integration tests\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## Grace at NDC Oslo 2023\r\n\r\nI gave my first conference talk about Grace at NDC Oslo 2023. Grace has changed _a lot_ since then, but this was the original idea. You can watch it [here](https://youtu.be/lW0gxMbyLEM):\r\n\r\n[<img src=\"https://github.com/ScottArbeit/Grace/assets/2406993/2f20bf3a-9907-42d3-8596-84a7e1334f55\">](https://youtu.be/lW0gxMbyLEM)\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## Contributing\r\n\r\nIf you want to help shape Grace:\r\n\r\n- Read `CONTRIBUTING.md`\r\n- AI submissions are expected and welcomed, but they have to follow\r\n- Open issues for rough edges, missing docs, or confusing workflows\r\n\r\n\r\n[^grace]: Definition excerpted from https://www.thefreedictionary.com/grace.\r\n"
  },
  {
    "path": "REPO_INDEX.md",
    "content": "# REPO_INDEX.md (Grace jump table)\r\n\r\nThis file is a machine-oriented index to help tools and humans quickly find the right code.\r\n\r\n---\r\n\r\n## Suggested search strategy for AllGrace.txt\r\n\r\n1. Refer to AllGrace_Index.md for exact starting and ending line numbers for each file.\r\n2. Use the entry points list below to decide where to navigate to.\r\n3. Jump to the exact starting line of the file, and search within the starting and ending line numbers.\r\n\r\n## Top entry points (open these first)\r\n\r\n### Grace Server (HTTP + DI + Orleans wiring)\r\n\r\n- `src/Grace.Server/Startup.Server.fs`\r\n  HTTP routes/endpoints and server composition entry points.\r\n- `src/Grace.Server/Program.Server.fs`\r\n  Host startup (Kestrel/Orleans host build/run).\r\n\r\n### Orleans grains (domain behavior)\r\n\r\n- `src/Grace.Actors/**/*.fs`\r\n  Grain/actor implementations. Look for `*Actor.fs` as primary behavior files.\r\n\r\n### Domain types, events, DTOs\r\n\r\n- `src/Grace.Types/**/*.fs`\r\n  Discriminated unions, events, DTOs, identifiers, serialization shapes.\r\n\r\n### Local orchestration (emulators/containers)\r\n\r\n- `src/Grace.Aspire.AppHost/Program.Aspire.AppHost.cs`\r\n  Local dev topology: Cosmos emulator, Azurite, Service Bus emulator, Redis, and Grace.Server.\r\n\r\n### Integration tests\r\n\r\n- `src/Grace.Server.Tests/General.Server.Tests.fs`\r\n  Test harness bootstrapping and shared test state.\r\n- `src/Grace.Server.Tests/Owner.Server.Tests.fs`\r\n  Owner API tests.\r\n- `src/Grace.Server.Tests/Repository.Server.Tests.fs`\r\n  Repository API tests.\r\n\r\n---\r\n\r\n## Cross-cutting �where is X implemented?�\r\n\r\n### JSON serialization settings\r\n\r\n- Search: `JsonSerializerOptions`\r\n- Likely in: `src/Grace.Shared/**` or `src/Grace.Server/**`\r\n\r\n### Service Bus publishing\r\n\r\n- Search: `publishGraceEvent`, `ServiceBusMessage`, `GraceEvent`\r\n- Likely in:\r\n  - `src/Grace.Actors/**` (where events are emitted)\r\n  - `src/Grace.Server/**` (wiring/config)\r\n  - `src/Grace.Shared/**` (helpers)\r\n\r\n### Cosmos DB / persistence wiring\r\n\r\n- Search: `AddCosmosGrainStorage`, `CosmosClient`, `UseAzureStorageClustering`\r\n- Likely in: `src/Grace.Server/Startup.Server.fs`\r\n\r\n### Azure Blob grain storage\r\n\r\n- Search: `AddAzureBlobGrainStorage`, `BlobServiceClient`\r\n- Likely in: `src/Grace.Server/Startup.Server.fs`\r\n\r\n### CLI commands\r\n\r\n- Search: `grace owner create`, `Command`, `System.CommandLine`\r\n- Likely in: `src/Grace.CLI/**`\r\n\r\n---\r\n\r\n## Local development �source of truth�\r\n\r\n- Aspire AppHost defines the runnable local environment. If there is a discrepancy between older docs/tests and AppHost, prefer AppHost.\r\n\r\n---\r\n\r\n## Obsolete / legacy systems\r\n\r\n- Dapr is not used anymore. Any Dapr references in tests or tooling are legacy and should be removed or ignored.\r\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\r\n\r\nThis project is currently **Alpha** quality. Security hardening is ongoing and may not yet cover all threat models.\r\n\r\n## Supported versions\r\n\r\nBecause this project is Alpha, only the **latest code on `main`** is supported.\r\n\r\n## Reporting a vulnerability\r\n\r\nIf you believe you have found a security issue, please report it by opening a **GitHub Issue** and applying the **`Security`** label.\r\n\r\nWhen filing a report, include as much of the following as you can:\r\n\r\n- A clear description of the issue and expected vs actual behavior\r\n- Steps to reproduce (minimal repro if possible)\r\n- Affected components, versions, configuration, and environment details\r\n- Impact assessment (what an attacker could do)\r\n- Any proof-of-concept code or logs (redact secrets)\r\n\r\nIf the issue includes sensitive information (tokens, credentials, private URLs, etc.), **do not post secrets**. Remove/rotate them before submitting.\r\n\r\n## What to expect\r\n\r\n- We will triage security reports as part of normal development.\r\n- Fixes will be prioritized alongside ongoing work based on severity and effort.\r\n- We may ask for clarification or additional reproduction details.\r\n\r\n## Security best practices for contributors\r\n\r\n- Do not commit secrets (API keys, connection strings, certificates) to the repository.\r\n- Prefer environment variables or a local secret store for development configuration.\r\n- Keep dependencies updated and avoid introducing unnecessary new dependencies.\r\n- If you introduce authentication/authorization or crypto-related changes, include tests.\r\n\r\n## Disclosure\r\n\r\nPlease avoid public disclosure details (including exploit steps) until a fix is available.\r\n\r\n"
  },
  {
    "path": "_endpoint_stub.txt",
    "content": "    endpoint \"GET\" \"/\" Authenticated\r\n    endpoint \"POST\" \"/access/checkPermission\" Authenticated\r\n    endpoint \"POST\" \"/access/grantRole\" Authenticated\r\n    endpoint \"POST\" \"/access/listPathPermissions\" Authenticated\r\n    endpoint \"POST\" \"/access/listRoleAssignments\" Authenticated\r\n    endpoint \"GET\" \"/access/listRoles\" Authenticated\r\n    endpoint \"POST\" \"/access/removePathPermission\" Authenticated\r\n    endpoint \"POST\" \"/access/revokeRole\" Authenticated\r\n    endpoint \"POST\" \"/access/upsertPathPermission\" Authenticated\r\n    endpoint \"POST\" \"/admin/deleteAllFromCosmosDB\" Authenticated\r\n    endpoint \"POST\" \"/admin/deleteAllRemindersFromCosmosDB\" Authenticated\r\n    endpoint \"GET\" \"/auth/login\" Authenticated\r\n    endpoint \"GET\" \"/auth/login/%s\" Authenticated\r\n    endpoint \"GET\" \"/auth/logout\" Authenticated\r\n    endpoint \"GET\" \"/auth/me\" Authenticated\r\n    endpoint \"GET\" \"/auth/oidc/config\" Authenticated\r\n    endpoint \"POST\" \"/auth/token/create\" Authenticated\r\n    endpoint \"POST\" \"/auth/token/list\" Authenticated\r\n    endpoint \"POST\" \"/auth/token/revoke\" Authenticated\r\n    endpoint \"POST\" \"/branch/assign\" Authenticated\r\n    endpoint \"POST\" \"/branch/checkpoint\" Authenticated\r\n    endpoint \"POST\" \"/branch/commit\" Authenticated\r\n    endpoint \"POST\" \"/branch/create\" Authenticated\r\n    endpoint \"POST\" \"/branch/createExternal\" Authenticated\r\n    endpoint \"POST\" \"/branch/delete\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableAssign\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableAutoRebase\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableCheckpoint\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableCommit\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableExternal\" Authenticated\r\n    endpoint \"POST\" \"/branch/enablePromotion\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableSave\" Authenticated\r\n    endpoint \"POST\" \"/branch/enableTag\" Authenticated\r\n    endpoint \"POST\" \"/branch/get\" Authenticated\r\n    endpoint \"POST\" \"/branch/getCheckpoints\" Authenticated\r\n    endpoint \"POST\" \"/branch/getCommits\" Authenticated\r\n    endpoint \"POST\" \"/branch/getDiffsForReferenceType\" Authenticated\r\n    endpoint \"POST\" \"/branch/getEvents\" Authenticated\r\n    endpoint \"POST\" \"/branch/getExternals\" Authenticated\r\n    endpoint \"POST\" \"/branch/getParentBranch\" Authenticated\r\n    endpoint \"POST\" \"/branch/getPromotions\" Authenticated\r\n    endpoint \"POST\" \"/branch/getRecursiveSize\" Authenticated\r\n    endpoint \"POST\" \"/branch/getReference\" Authenticated\r\n    endpoint \"POST\" \"/branch/getReferences\" Authenticated\r\n    endpoint \"POST\" \"/branch/getSaves\" Authenticated\r\n    endpoint \"POST\" \"/branch/getTags\" Authenticated\r\n    endpoint \"POST\" \"/branch/getVersion\" Authenticated\r\n    endpoint \"POST\" \"/branch/listContents\" Authenticated\r\n    endpoint \"POST\" \"/branch/promote\" Authenticated\r\n    endpoint \"POST\" \"/branch/rebase\" Authenticated\r\n    endpoint \"POST\" \"/branch/save\" Authenticated\r\n    endpoint \"POST\" \"/branch/setPromotionMode\" Authenticated\r\n    endpoint \"POST\" \"/branch/tag\" Authenticated\r\n    endpoint \"POST\" \"/branch/updateParentBranch\" Authenticated\r\n    endpoint \"POST\" \"/candidate/attestations\" Authenticated\r\n    endpoint \"POST\" \"/candidate/cancel\" Authenticated\r\n    endpoint \"POST\" \"/candidate/gate/rerun\" Authenticated\r\n    endpoint \"POST\" \"/candidate/get\" Authenticated\r\n    endpoint \"POST\" \"/candidate/required-actions\" Authenticated\r\n    endpoint \"POST\" \"/candidate/retry\" Authenticated\r\n    endpoint \"POST\" \"/diff/getDiff\" Authenticated\r\n    endpoint \"POST\" \"/diff/getDiffBySha256Hash\" Authenticated\r\n    endpoint \"POST\" \"/diff/populate\" Authenticated\r\n    endpoint \"POST\" \"/directory/create\" Authenticated\r\n    endpoint \"POST\" \"/directory/get\" Authenticated\r\n    endpoint \"POST\" \"/directory/getByDirectoryIds\" Authenticated\r\n    endpoint \"POST\" \"/directory/getBySha256Hash\" Authenticated\r\n    endpoint \"POST\" \"/directory/getDirectoryVersionsRecursive\" Authenticated\r\n    endpoint \"POST\" \"/directory/getZipFile\" Authenticated\r\n    endpoint \"POST\" \"/directory/saveDirectoryVersions\" Authenticated\r\n    endpoint \"GET\" \"/healthz\" Authenticated\r\n    endpoint \"POST\" \"/organization/create\" Authenticated\r\n    endpoint \"POST\" \"/organization/delete\" Authenticated\r\n    endpoint \"POST\" \"/organization/get\" Authenticated\r\n    endpoint \"POST\" \"/organization/listRepositories\" Authenticated\r\n    endpoint \"POST\" \"/organization/setDescription\" Authenticated\r\n    endpoint \"POST\" \"/organization/setName\" Authenticated\r\n    endpoint \"POST\" \"/organization/setSearchVisibility\" Authenticated\r\n    endpoint \"POST\" \"/organization/setType\" Authenticated\r\n    endpoint \"POST\" \"/organization/undelete\" Authenticated\r\n    endpoint \"POST\" \"/owner/create\" Authenticated\r\n    endpoint \"POST\" \"/owner/delete\" Authenticated\r\n    endpoint \"POST\" \"/owner/get\" Authenticated\r\n    endpoint \"POST\" \"/owner/listOrganizations\" Authenticated\r\n    endpoint \"POST\" \"/owner/setDescription\" Authenticated\r\n    endpoint \"POST\" \"/owner/setName\" Authenticated\r\n    endpoint \"POST\" \"/owner/setSearchVisibility\" Authenticated\r\n    endpoint \"POST\" \"/owner/setType\" Authenticated\r\n    endpoint \"POST\" \"/owner/undelete\" Authenticated\r\n    endpoint \"POST\" \"/policy/acknowledge\" Authenticated\r\n    endpoint \"POST\" \"/policy/current\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/addPromotion\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/block\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/complete\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/create\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/delete\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/get\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/getEvents\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/markReady\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/removePromotion\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/reorderPromotions\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/schedule\" Authenticated\r\n    endpoint \"POST\" \"/promotionGroup/start\" Authenticated\r\n    endpoint \"POST\" \"/queue/dequeue\" Authenticated\r\n    endpoint \"POST\" \"/queue/enqueue\" Authenticated\r\n    endpoint \"POST\" \"/queue/pause\" Authenticated\r\n    endpoint \"POST\" \"/queue/resume\" Authenticated\r\n    endpoint \"POST\" \"/queue/status\" Authenticated\r\n    endpoint \"POST\" \"/reminder/create\" Authenticated\r\n    endpoint \"POST\" \"/reminder/delete\" Authenticated\r\n    endpoint \"POST\" \"/reminder/get\" Authenticated\r\n    endpoint \"POST\" \"/reminder/list\" Authenticated\r\n    endpoint \"POST\" \"/reminder/reschedule\" Authenticated\r\n    endpoint \"POST\" \"/reminder/updateTime\" Authenticated\r\n    endpoint \"POST\" \"/repository/create\" Authenticated\r\n    endpoint \"POST\" \"/repository/delete\" Authenticated\r\n    endpoint \"POST\" \"/repository/exists\" Authenticated\r\n    endpoint \"POST\" \"/repository/get\" Authenticated\r\n    endpoint \"POST\" \"/repository/getBranches\" Authenticated\r\n    endpoint \"POST\" \"/repository/getBranchesByBranchId\" Authenticated\r\n    endpoint \"POST\" \"/repository/getReferencesByReferenceId\" Authenticated\r\n    endpoint \"POST\" \"/repository/isEmpty\" Authenticated\r\n    endpoint \"POST\" \"/repository/setAllowsLargeFiles\" Authenticated\r\n    endpoint \"POST\" \"/repository/setAnonymousAccess\" Authenticated\r\n    endpoint \"POST\" \"/repository/setCheckpointDays\" Authenticated\r\n    endpoint \"POST\" \"/repository/setConflictResolutionPolicy\" Authenticated\r\n    endpoint \"POST\" \"/repository/setDefaultServerApiVersion\" Authenticated\r\n    endpoint \"POST\" \"/repository/setDescription\" Authenticated\r\n    endpoint \"POST\" \"/repository/setDiffCacheDays\" Authenticated\r\n    endpoint \"POST\" \"/repository/setDirectoryVersionCacheDays\" Authenticated\r\n    endpoint \"POST\" \"/repository/setLogicalDeleteDays\" Authenticated\r\n    endpoint \"POST\" \"/repository/setName\" Authenticated\r\n    endpoint \"POST\" \"/repository/setRecordSaves\" Authenticated\r\n    endpoint \"POST\" \"/repository/setSaveDays\" Authenticated\r\n    endpoint \"POST\" \"/repository/setStatus\" Authenticated\r\n    endpoint \"POST\" \"/repository/setVisibility\" Authenticated\r\n    endpoint \"POST\" \"/repository/undelete\" Authenticated\r\n    endpoint \"POST\" \"/review/checkpoint\" Authenticated\r\n    endpoint \"POST\" \"/review/deepen\" Authenticated\r\n    endpoint \"POST\" \"/review/packet\" Authenticated\r\n    endpoint \"POST\" \"/review/resolve\" Authenticated\r\n    endpoint \"POST\" \"/storage/getDownloadUri\" Authenticated\r\n    endpoint \"POST\" \"/storage/getUploadMetadataForFiles\" Authenticated\r\n    endpoint \"POST\" \"/storage/getUploadUri\" Authenticated\r\n    endpoint \"POST\" \"/work/create\" Authenticated\r\n    endpoint \"POST\" \"/work/get\" Authenticated\r\n    endpoint \"POST\" \"/work/link/promotion-group\" Authenticated\r\n    endpoint \"POST\" \"/work/link/reference\" Authenticated\r\n    endpoint \"POST\" \"/work/update\" Authenticated\r\n"
  },
  {
    "path": "code_of_conduct.md",
    "content": "# Grace - Code of Conduct\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## Culture\r\n\r\nCulture is not a physical thing. You can't _touch_ culture. You can see it reflected in things like visual arts, clothing, books, code, UX, and so much else, but culture _itself_ is not an exterior object that can be seen and measured with some scientific apparatus.\r\n\r\nCulture is _interior_; it lives _inside_ each member of a group. It's what it means when we say \"we\". It's first-person, plural. Culture is a set of stories or perspectives, shared by a group, that help to define both _how to behave_ within that group, and _how it feels_ to be part of that group.\r\n\r\nI want to create a culture at Grace that lives up to its name. We all deserve it.\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n## The name of this project is _Grace_.\r\n\r\nBe **graceful** in your interactions here.\r\n\r\nGive **grace** to everyone participating with us.\r\n\r\nCreate something together that embodies **grace** in its design and form.\r\n\r\nWhen in doubt, **_remember the name of the project._**\r\n\r\n![](./Assets/Orange3.svg)\r\n\r\n\r\nTo be clear: if your behavior is more disruptive than constructive, you will be asked to leave.\r\n"
  },
  {
    "path": "docs/.markdownlint.jsonc",
    "content": "{\n  // For the /docs directory, disable line length rule. We're writing prose.\n  \"MD013\": {\n    \"enabled\": false\n  }\n}\n"
  },
  {
    "path": "docs/AI Submissions.md",
    "content": "# AI Submissions for Grace\n\nGrace welcomes AI-assisted contributions when they are transparent, reproducible, and technically strong.\n\n## Why This Exists\n\nMany projects ban AI-generated pull requests because low-quality submissions create review overhead.\nGrace takes a different approach: AI assistance is welcomed, but only with clear disclosure, a high quality bar,\nand standardized prompt outputs to make review as easy and as deterministic as possible.\n\n## What We Invite\n\nWe want your contributions, both in issues and in pull requests. We just want you to run our prompts when you work on\nthose contributions to make review as streamlined as possible.\n\nPlease start with an issue for alignment on features and direction. Once the issue and new requirements are agreed on,\nyour contributions are welcomed. We don't want any \"I have a huge change but didn't discuss it with anyone\nfirst\" contributions.\n\nUse your preferred coding agent and provider to investigate issues, implement changes, and draft contribution materials.\n\nFor issues, use your agent to investigate the existing code and to develop an idea for a change. When you're ready\nto submit the issue, you MUST use the [Grace issue summary](/prompts/Grace%20issue%20summary.md) prompt to create the\nissue description.\n\nFor pull requests, when you're satisfied that your work is done and fully tested, you MUST use the [Grace pull request summary](/prompts/Grace%20pull%20request%20summary.md)\nprompt with your coding agent to create the pull request description.\n\nThese standardized formats will help the maintainers of Grace keep up with everything you throw at us. (We hope.)\n\n## Be Cool\n\nPlease treat the submission like professional engineering work product that you're proud of. ❤️\n\n## Non-Negotiable Submission Rules\n\n1. Use the latest and most powerful generally available reasoning model from your chosen provider at submission time.\n   (i.e. Opus, not Sonnet; nothing with \"mini\" in the name) [^models]\n2. Use reasoning at least equivalent to Codex and Claude's `high` (or stronger) to plan and implement the work.\n3. Use the required Grace issue and pull request template prompts to have your agent automatically create the submission\n   package.\n   - Issue description prompt: [Grace issue summary.md](/prompts/Grace%20issue%20summary.md)\n   - Pull request description prompt: [Grace pull request summary.md](/prompts/Grace%20pull%20request%20summary.md)\n4. Include the prompts used to produce the final result.\n5. Assume your output will be re-researched and reviewed by other LLMs and humans.\n\n[^models]: I realize that this creates a barrier for AI submission that includes only those who can afford to run frontier models.\nThat's the minimum quality I need in the first half of 2026. I expect that by 2027 \"Sonnet\" and \"Mini\" level models will achieve a\nsimilar capability level, and I will adjust these requirements when they're ready.\n\nWrite-ups should:\n\n- Be clear.\n- Be explicit.\n- Be thorough.\n- Prefer evidence over vague claims.\n\n## Reasoning Level Mapping Guidance\n\nYour provider may use different labels. Map your configuration to the closest equivalent that is at least Codex or Claude\n`high`.\n\n- OpenAI: `high` or `xhigh` reasoning.\n- Anthropic: `high` or `max` effort.\n- Google Gemini: thinking enabled with a high/maximum reasoning configuration.\n- Other providers: choose the highest non-experimental reasoning mode generally available for production use.\n\nIf your provider exposes only vague labels, document exactly what you selected and why it is equivalent or stronger\nthan `high`.\n\n## Quality Expectations\n\nA compliant submission should make reviewer verification fast. The summary prompts are designed to let you create high-quality,\ncomprehensive submissions that validate that you've done the work correctly, and that communicate its value clearly to reviewers.\n\n## Review and Enforcement\n\nSubmissions can be closed or requested for revision when:\n\n1. Model/reasoning metadata is missing or unclear.\n2. The run does not meet the latest-model or high-reasoning rule.\n3. Prompt history is omitted.\n4. The write-up is shallow, unverifiable, or inconsistent with the code.\n"
  },
  {
    "path": "docs/Authentication.md",
    "content": "# Authentication\r\n\r\n<!-- markdownlint-configure-file {\"MD013\": {\"line_length\": 120}} -->\r\n\r\nThis document describes how to configure and use authentication for Grace during development.\r\n\r\nGrace supports two authentication mechanisms:\r\n\r\n1. **Auth0 (OIDC/JWT bearer tokens)** for interactive developer login (PKCE or Device Code) and\r\n   machine-to-machine (client credentials).\r\n1. **Grace Personal Access Tokens (PATs)** for automation and non-interactive usage.\r\n\r\n> Important: Authentication proves “who you are.” Authorization (RBAC and path permissions) determines\r\n> “what you can do.” PATs do **not** introduce a separate permission model; they authenticate a principal\r\n> that is then authorized via Grace’s normal authorization system.\r\n\r\n---\r\n\r\n## Quickstart for contributors (recommended path)\r\n\r\n1. **Set up Auth0** (one-time) using the instructions in **Auth0 tenant setup** below.\r\n1. **Run Grace.Server** (typically via Aspire) with OIDC env vars configured.\r\n1. **Point the Grace CLI at your server** by setting `GRACE_SERVER_URI`.\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$env:GRACE_SERVER_URI=\"http://localhost:5000\"\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nexport GRACE_SERVER_URI=\"http://localhost:5000\"\r\n```\r\n\r\n1. **Login via the CLI**:\r\n\r\n   * `grace auth login` — Interactive login (tries PKCE first, then falls back to Device Code).\r\n   * `grace auth whoami` — Verifies your identity against the running server.\r\n\r\n---\r\n\r\n## Authorization bootstrap (SystemAdmin seeding)\r\n\r\nGrace supports a one-time bootstrap mechanism to seed the first SystemAdmin role assignment in a fresh environment.\r\nThis is required when you are standing up a new deployment that has no RBAC assignments yet.\r\n\r\n### How it works\r\n\r\n* Bootstrap runs **only** when the system-scope AccessControl actor first activates and has **no** existing assignments.\r\n* It reads configured bootstrap principals and creates `SystemAdmin` role assignments at `Scope.System`.\r\n* Bootstrap is **one-time** and will **never** overwrite or re-seed once any system-scope assignments exist.\r\n\r\n### Configuration\r\n\r\nSet one or both of these environment variables (semicolon-delimited list):\r\n\r\n* `grace__authz__bootstrap__system_admin_users`\r\n* `grace__authz__bootstrap__system_admin_groups`\r\n\r\n### Important\r\n\r\n* The values must be **principal IDs**, not emails or display names.\r\n* For Auth0/OIDC, the user principal ID is taken from the `sub` claim (exposed as `grace_user_id`).\r\n* You can confirm the user ID with `GET /auth/me` (`GraceUserId` in the response).\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$env:grace__authz__bootstrap__system_admin_users=\"auth0|abc123\"\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nexport grace__authz__bootstrap__system_admin_users=\"auth0|abc123\"\r\n```\r\n\r\n---\r\n\r\n## Development without Auth0 (TestAuth)\r\n\r\nFor local development, you can skip Auth0 entirely by enabling the built-in TestAuth handler.\r\nThis is intended for **dev/test only**.\r\n\r\n### Enable TestAuth\r\n\r\nSet:\r\n\r\n* `GRACE_TESTING=1` (also accepts `true` or `yes`)\r\n\r\nWhen enabled, Grace authenticates requests using headers:\r\n\r\n* `x-grace-user-id` — required; becomes the user principal ID (`grace_user_id`).\r\n* `x-grace-claims` — optional; semicolon-delimited values mapped to `grace_claim`.\r\n\r\n### Bootstrap as SystemAdmin without Auth0\r\n\r\n1. Choose a local user ID (e.g., `dev-scott`).\r\n1. Set bootstrap to that user ID:\r\n\r\n   PowerShell:\r\n\r\n   ```powershell\r\n   $env:grace__authz__bootstrap__system_admin_users=\"dev-scott\"\r\n   ```\r\n\r\n   bash / zsh:\r\n\r\n   ```bash\r\n   export grace__authz__bootstrap__system_admin_users=\"dev-scott\"\r\n   ```\r\n\r\n1. Send requests with the `x-grace-user-id` header.\r\n\r\n   PowerShell:\r\n\r\n   ```powershell\r\n   Invoke-RestMethod \"http://localhost:5000/auth/me\" -Headers @{ \"x-grace-user-id\" = \"dev-scott\" }\r\n   ```\r\n\r\n   bash / zsh:\r\n\r\n   ```bash\r\n   curl -H \"x-grace-user-id: dev-scott\" \"http://localhost:5000/auth/me\"\r\n   ```\r\n\r\nOn first activation (with no existing assignments), Grace will seed `SystemAdmin` for `dev-scott`.\r\nAfter that, bootstrap is a no-op.\r\n\r\n---\r\n\r\n## Auth0 tenant setup (development)\r\n\r\n### What you need to create in Auth0\r\n\r\nGrace needs these Auth0 resources:\r\n\r\n1. **An Auth0 API (Resource Server)** for the Grace Server API\r\n\r\n   * This provides the **Audience** value (the API Identifier).\r\n1. **A Native Auth0 Application** for the Grace CLI (interactive login)\r\n\r\n   * This provides the **CLI client ID**.\r\n1. *(Optional)* **A Machine-to-Machine Auth0 Application** for CI/automation\r\n\r\n   * This provides an **M2M client ID** and **M2M client secret**.\r\n\r\n### Values you must capture from Auth0 (you will use these as env vars)\r\n\r\n* **Tenant domain** (example: `my-tenant.us.auth0.com`)\r\n\r\n  * Used to construct the **Authority**: `https://<tenant-domain>/`\r\n* **API Identifier** (your “Audience”)\r\n\r\n  * Example: `https://grace.local/api`\r\n* **Native app Client ID** (Grace CLI interactive login)\r\n* *(Optional)* **M2M app Client ID + Client Secret**\r\n\r\n---\r\n\r\n### Step 1: Create the Auth0 API (Resource Server)\r\n\r\nAuth0 Dashboard steps:\r\n\r\n1. Go to **Applications → APIs → Create API**.\r\n1. Set:\r\n\r\n   * **Name**: `Grace (Dev)` (or similar)\r\n   * **Identifier** (this becomes the **Audience**): choose a stable string, e.g. `https://grace.local/api`\r\n1. Enable **Allow Offline Access** on the API.\r\n\r\n   * This is required so the CLI can receive refresh tokens (when requesting `offline_access`).\r\n1. Save.\r\n\r\nRecord:\r\n\r\n* API **Identifier** (Audience)\r\n* Tenant Domain (Authority base)\r\n\r\n---\r\n\r\n### Step 2: Create the Auth0 Native Application (Grace CLI)\r\n\r\nAuth0 Dashboard steps:\r\n\r\n1. Go to **Applications → Applications → Create Application**.\r\n\r\n1. Choose application type: **Native**.\r\n\r\n1. In the application settings:\r\n\r\n   * Ensure **Authorization Code** (PKCE) is enabled.\r\n   * Ensure **Refresh Token** grant is enabled.\r\n   * Ensure **Device Code** grant is enabled (so the CLI can use device flow on headless systems).\r\n\r\n1. Configure **Allowed Callback URLs** to include the CLI callback URL:\r\n\r\n   * Default Grace CLI callback URL:\r\n\r\n     * `http://127.0.0.1:8391/callback`\r\n\r\n   If you override the CLI redirect port (via `grace__auth__oidc__cli_redirect_port`), you must also\r\n   update this callback URL accordingly.\r\n\r\n1. Configure refresh token behavior (recommended for development):\r\n\r\n   * Enable refresh token rotation (or equivalent Auth0 setting) and ensure refresh tokens are issued to\r\n     the application.\r\n\r\nRecord:\r\n\r\n* Native application **Client ID** (this is `grace__auth__oidc__cli_client_id`)\r\n\r\n---\r\n\r\n### Step 3 (optional): Create the Auth0 M2M Application (automation)\r\n\r\nAuth0 Dashboard steps:\r\n\r\n1. Create a new application of type **Machine to Machine**.\r\n1. Authorize it to call your **Grace API (Resource Server)**.\r\n1. Choose scopes if you’ve defined API scopes (Grace does not currently require Auth0 API scopes for\r\n   authorization decisions, but your tenant policies may).\r\n1. Record:\r\n\r\n   * **Client ID** (`grace__auth__oidc__m2m_client_id`)\r\n   * **Client Secret** (`grace__auth__oidc__m2m_client_secret`)\r\n\r\n---\r\n\r\n## Grace CLI authentication modes\r\n\r\nThe CLI can authenticate in multiple ways. The first matching mode “wins”:\r\n\r\n1. **PAT mode** if `GRACE_TOKEN` is set (must be a Grace PAT, prefix `grace_pat_v1_`).\r\n1. **Error** if `GRACE_TOKEN_FILE` is set (local token storage is intentionally disabled).\r\n1. **M2M mode** if M2M env vars are set.\r\n1. **Interactive mode** if you have logged in previously (token stored in OS secure store).\r\n\r\n### Primary CLI commands\r\n\r\n* `grace auth login [--auth pkce|device]`\r\n  Interactive login to Auth0; stores access/refresh tokens in the OS secure store. If `--auth` is not\r\n  specified, the CLI attempts PKCE and falls back to Device Code.\r\n\r\n* `grace auth status`\r\n  Shows whether the CLI currently has usable credentials (PAT, M2M, or interactive).\r\n\r\n* `grace auth whoami`\r\n  Calls the server and prints the authenticated identity information.\r\n\r\n* `grace auth logout`\r\n  Clears the cached interactive token from the secure store.\r\n\r\n---\r\n\r\n## Personal Access Tokens (PATs)\r\n\r\nPATs are bearer tokens issued by Grace and validated by Grace. They are typically used for:\r\n\r\n* CI jobs and automation\r\n* running the CLI in non-interactive environments\r\n* scripting against the Grace HTTP API\r\n\r\nA PAT string looks like:\r\n\r\n* `grace_pat_v1_<...>`\r\n\r\n### Creating a PAT\r\n\r\nYou must already be authenticated (interactive Auth0 login, or an existing PAT, or M2M) to create a PAT.\r\n\r\n#### CLI (recommended)\r\n\r\n* `grace auth token create --name \"<token-name>\"`\r\n  Creates a PAT with the server-default lifetime.\r\n\r\n* `grace auth token create --name \"<token-name>\" --expires-in 30d`\r\n  Creates a PAT that expires after the given duration. Supported suffixes: `s`, `m`, `h`, `d`.\r\n\r\n* `grace auth token create --name \"<token-name>\" --no-expiry`\r\n  Creates a non-expiring PAT **only if** the server allows it.\r\n\r\n#### Notes\r\n\r\n* The PAT value is a secret. Store it in a secret manager.\r\n* Treat PATs like passwords; do not commit them into git.\r\n\r\n### Using a PAT\r\n\r\nSet the token in your environment:\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$env:GRACE_TOKEN=\"grace_pat_v1_...\"\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nexport GRACE_TOKEN=\"grace_pat_v1_...\"\r\n```\r\n\r\nThen run any CLI command as usual; authentication will use the PAT automatically.\r\n\r\nIf you are calling the HTTP API directly, use the standard Authorization header:\r\n\r\n* `Authorization: Bearer grace_pat_v1_...`\r\n\r\n### Listing and revoking PATs\r\n\r\n* `grace auth token list`\r\n  Lists active PATs for the current principal.\r\n\r\n* `grace auth token list --all`\r\n  Lists active + expired + revoked tokens.\r\n\r\n* `grace auth token revoke <token-id>`\r\n  Revokes a PAT by token ID (GUID). Revoked tokens are no longer accepted.\r\n\r\n* `grace auth token status`\r\n  Shows whether the current `GRACE_TOKEN` is present and parseable.\r\n\r\n### How PAT “permissions” work in Grace\r\n\r\nPATs do **not** have an independent “permission set” like some systems (GitHub fine-grained tokens, etc.). Instead:\r\n\r\n* A PAT authenticates a principal (usually a user).\r\n* Grace authorization is then evaluated normally:\r\n\r\n  * **RBAC roles** assigned to the principal (user or group) at a scope\r\n  * **Path permissions** keyed by “claims” and/or group membership\r\n\r\nImportant implementation detail:\r\n\r\n* When a PAT is created, Grace snapshots the current principal’s `grace_claim` values and `grace_group_id`\r\n  values into the token record on the server.\r\n* If your group membership or claim set changes later, existing PATs will **not** automatically pick up\r\n  those changes. Create a new PAT if you need a token that reflects updated claims/groups.\r\n\r\n### Setting permissions for a PAT\r\n\r\nBecause a PAT’s authorization comes from the principal it authenticates, you “set PAT permissions” by\r\ngranting/revoking roles and path permissions for that principal.\r\n\r\nPrimary CLI commands for authorization management:\r\n\r\n* `grace access grant-role ...`\r\n  Grants a role to a principal at a scope (Owner/Organization/Repository/Branch/System).\r\n\r\n* `grace access revoke-role ...`\r\n  Revokes a role from a principal at a scope.\r\n\r\n* `grace access list-role-assignments ...`\r\n  Lists role assignments at a scope (optionally filtered by principal).\r\n\r\n* `grace access upsert-path-permission ...`\r\n  Sets or updates a path permission entry in a repository.\r\n\r\n* `grace access remove-path-permission ...`\r\n  Removes a path permission entry.\r\n\r\n* `grace access list-path-permissions ...`\r\n  Lists path permissions, optionally scoped to a path prefix.\r\n\r\n* `grace access check ...`\r\n  Asks the server “would this principal be allowed to do operation X on resource Y?”\r\n\r\n> For detailed role IDs and operations, use `grace access list-roles` and consult the authorization types\r\n> in `Grace.Types.Authorization`.\r\n\r\n---\r\n\r\n## Environment variables\r\n\r\nThis section documents auth-related environment variables used by Grace Server and the Grace CLI.\r\n\r\n### Grace CLI (always relevant)\r\n\r\n* `GRACE_SERVER_URI` (required for CLI)\r\n  **No default.** Must point to the running Grace server base URI.\r\n  Source: Aspire dashboard output, local run output, or your deployment URL.\r\n\r\nExample value:\r\n\r\n* `http://localhost:5000`\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$env:GRACE_SERVER_URI=\"http://localhost:5000\"\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nexport GRACE_SERVER_URI=\"http://localhost:5000\"\r\n```\r\n\r\n---\r\n\r\n### Grace Server OIDC configuration (Auth0/JWT)\r\n\r\nThese variables enable Auth0 JWT authentication on the server.\r\n\r\n* `grace__auth__oidc__authority` (optional, enables OIDC when set)\r\n  **No default.**\r\n  Source: your Auth0 tenant domain. Use `https://<tenant-domain>/`.\r\n\r\n* `grace__auth__oidc__audience` (required if `...__authority` is set)\r\n  **No default.**\r\n  Source: Auth0 API Identifier (Resource Server “Identifier”).\r\n\r\nRecommended (for CLI auto-config):\r\n\r\n* `grace__auth__oidc__cli_client_id` (optional for server auth; required for `/auth/oidc/config`)\r\n  **No default.**\r\n  Source: Auth0 Native app Client ID.\r\n\r\n> If `grace__auth__oidc__authority` and `grace__auth__oidc__audience` are not set, the server will fall\r\n> back to PAT-only authentication.\r\n\r\n---\r\n\r\n### Grace CLI interactive OIDC configuration (Auth0 login)\r\n\r\nThe Grace CLI can authenticate interactively using Auth0 (OIDC). There are two ways to supply the required\r\nOIDC settings:\r\n\r\n1. **Recommended:** have the CLI fetch OIDC settings from the Grace Server.\r\n1. **Advanced:** configure OIDC settings directly on the CLI via environment variables.\r\n\r\n---\r\n\r\n#### Recommended: CLI auto-configuration from the server\r\n\r\nIf you set only:\r\n\r\n* `GRACE_SERVER_URI` — Base URI of the running Grace Server (example: `http://localhost:5000`)\r\n\r\n…then the CLI can fetch the OIDC configuration automatically by calling:\r\n\r\n* `GET /auth/oidc/config` — Returns the server’s OIDC settings needed for interactive login.\r\n\r\nThis works **only if** the server is configured with OIDC and has the values needed to publish them (see\r\nserver env vars below).\r\n\r\n##### How to use (server auto-configuration)\r\n\r\n1. Start the server with OIDC enabled (Authority + Audience, and preferably the CLI client ID).\r\n\r\n1. Set the server URI:\r\n\r\n   PowerShell:\r\n\r\n   ```powershell\r\n   $env:GRACE_SERVER_URI=\"http://localhost:5000\"\r\n   ```\r\n\r\n   bash / zsh:\r\n\r\n   ```bash\r\n   export GRACE_SERVER_URI=\"http://localhost:5000\"\r\n   ```\r\n\r\n1. Login:\r\n\r\n   * `grace auth login` — Interactive Auth0 login (tries PKCE, then device flow).\r\n\r\n1. Verify:\r\n\r\n   * `grace auth whoami` — Calls the server and prints the authenticated identity.\r\n\r\n##### Server-side requirements for auto-configuration\r\n\r\nFor `GET /auth/oidc/config` to return useful values, the server must be configured with:\r\n\r\n* `grace__auth__oidc__authority` — `https://<tenant-domain>/`\r\n* `grace__auth__oidc__audience` — Auth0 API Identifier\r\n* `grace__auth__oidc__cli_client_id` — Auth0 Native app Client ID (recommended)\r\n\r\nIf the server is missing `grace__auth__oidc__cli_client_id`, the CLI may still be able to login if you\r\nsupply the client ID locally (see Advanced).\r\n\r\n---\r\n\r\n#### Advanced: set OIDC settings on the client\r\n\r\nIf you cannot use server auto-configuration (for example, you are testing against an endpoint that does\r\nnot expose `/auth/oidc/config`), you can configure the CLI directly via environment variables.\r\n\r\n##### Required\r\n\r\n* `grace__auth__oidc__authority`\r\n  No default. Source: Auth0 tenant domain.\r\n  Format: `https://<tenant-domain>/`\r\n\r\n* `grace__auth__oidc__audience`\r\n  No default. Source: Auth0 API Identifier (Resource Server “Identifier”).\r\n\r\n* `grace__auth__oidc__cli_client_id`\r\n  No default. Source: Auth0 Native app Client ID.\r\n\r\n##### Optional (recommended defaults)\r\n\r\n* `grace__auth__oidc__cli_redirect_port`\r\n  Default: `8391`\r\n  If you change this, you must also update the Auth0 Native app callback URL to:\r\n  `http://127.0.0.1:<port>/callback`\r\n\r\n* `grace__auth__oidc__cli_scopes`\r\n  Default: `openid profile email offline_access`\r\n  `offline_access` is required to receive refresh tokens.\r\n\r\n##### How to use (client configuration)\r\n\r\n1. Set environment variables.\r\n\r\n   PowerShell:\r\n\r\n   ```powershell\r\n   $env:GRACE_SERVER_URI=\"http://localhost:5000\"\r\n   $env:grace__auth__oidc__authority=\"https://<tenant-domain>/\"\r\n   $env:grace__auth__oidc__audience=\"https://grace.local/api\"\r\n   $env:grace__auth__oidc__cli_client_id=\"<native-client-id>\"\r\n   ```\r\n\r\n   bash / zsh:\r\n\r\n   ```bash\r\n   export GRACE_SERVER_URI=\"http://localhost:5000\"\r\n   export grace__auth__oidc__authority=\"https://<tenant-domain>/\"\r\n   export grace__auth__oidc__audience=\"https://grace.local/api\"\r\n   export grace__auth__oidc__cli_client_id=\"<native-client-id>\"\r\n   ```\r\n\r\n1. Login:\r\n\r\n   * `grace auth login` — Interactive Auth0 login (tries PKCE, then device flow).\r\n\r\n1. Verify:\r\n\r\n   * `grace auth whoami` — Calls the server and prints the authenticated identity.\r\n\r\n---\r\n\r\n### Grace CLI machine-to-machine (M2M) configuration\r\n\r\nIf you set these env vars, the CLI will use Auth0 client credentials to obtain an access token.\r\n\r\n* `grace__auth__oidc__authority`\r\n  **No default.** Source: Auth0 tenant domain.\r\n\r\n* `grace__auth__oidc__audience`\r\n  **No default.** Source: Auth0 API Identifier.\r\n\r\n* `grace__auth__oidc__m2m_client_id`\r\n  **No default.** Source: Auth0 M2M application Client ID.\r\n\r\n* `grace__auth__oidc__m2m_client_secret`\r\n  **No default.** Source: Auth0 M2M application Client Secret.\r\n\r\nOptional:\r\n\r\n* `grace__auth__oidc__m2m_scopes`\r\n  Default: empty\r\n  Space-separated list of scopes to request (if your tenant requires/uses them).\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$env:grace__auth__oidc__authority=\"https://<tenant-domain>/\"\r\n$env:grace__auth__oidc__audience=\"https://grace.local/api\"\r\n$env:grace__auth__oidc__m2m_client_id=\"<m2m-client-id>\"\r\n$env:grace__auth__oidc__m2m_client_secret=\"<m2m-client-secret>\"\r\n# Optional:\r\n$env:grace__auth__oidc__m2m_scopes=\"read:foo write:bar\"\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nexport grace__auth__oidc__authority=\"https://<tenant-domain>/\"\r\nexport grace__auth__oidc__audience=\"https://grace.local/api\"\r\nexport grace__auth__oidc__m2m_client_id=\"<m2m-client-id>\"\r\nexport grace__auth__oidc__m2m_client_secret=\"<m2m-client-secret>\"\r\n# Optional:\r\nexport grace__auth__oidc__m2m_scopes=\"read:foo write:bar\"\r\n```\r\n\r\n---\r\n\r\n### Grace CLI PAT configuration\r\n\r\n* `GRACE_TOKEN` (optional)\r\n  **No default.**\r\n  Source: output from `grace auth token create`.\r\n  Must be a Grace PAT (prefix `grace_pat_v1_`). If set, this overrides interactive login and M2M.\r\n\r\n* `GRACE_TOKEN_FILE`\r\n  **Not supported.** Local plaintext token file storage is intentionally disabled.\r\n\r\n---\r\n\r\n### Grace Server PAT policy controls\r\n\r\nThese affect how the server handles PAT creation requests:\r\n\r\n* `grace__auth__pat__default_lifetime_days`\r\n  Default: `90`\r\n\r\n* `grace__auth__pat__max_lifetime_days`\r\n  Default: `365`\r\n\r\n* `grace__auth__pat__allow_no_expiry`\r\n  Default: `false`\r\n\r\n---\r\n\r\n## Getting Auth0 values automatically (CLI + APIs)\r\n\r\nIf you already have an Auth0 tenant configured, the **Auth0 CLI** can be used to find the exact values\r\nneeded for Grace without clicking through the dashboard.\r\n\r\n### Auth0 CLI login\r\n\r\n* `auth0 login`\r\n  Authenticates the Auth0 CLI for interactive use (or with client credentials for CI).\r\n\r\nIf you need additional Management API scopes, re-run login with scopes, e.g.:\r\n\r\n* `auth0 login --scopes \"read:client_grants,create:client_grants\"`\r\n\r\n### Find tenant information\r\n\r\n* `auth0 tenants list --json`\r\n  Lists tenants accessible to your Auth0 CLI session.\r\n\r\n* `auth0 tenant-settings show --json`\r\n  Shows tenant settings (useful to confirm you’re operating on the expected tenant).\r\n\r\n### Find the Grace API audience (API Identifier)\r\n\r\n* `auth0 apis list --json`\r\n  Lists APIs (Resource Servers). Find your Grace API and read its `identifier`.\r\n\r\n* `auth0 apis show <api-id|api-audience> --json`\r\n  Shows details for a specific API.\r\n\r\n### Find the CLI Client ID (Native app) and M2M credentials\r\n\r\n* `auth0 apps list --json`\r\n  Lists applications.\r\n\r\n* `auth0 apps show <app-id> --json`\r\n  Shows app details (including `client_id`).\r\n\r\n* `auth0 apps show <app-id> --reveal-secrets --json`\r\n  Shows app details **including secrets** (use only for M2M apps, and handle output carefully).\r\n\r\n### Make raw Management API calls (advanced)\r\n\r\n* `auth0 api get \"tenants/settings\"`\r\n  Makes an authenticated request to the Auth0 Management API and prints JSON.\r\n\r\nThis is useful if you need endpoints not exposed by a dedicated `auth0 <noun> <verb>` command.\r\n\r\n### Creating Auth0 resources via the Auth0 CLI (optional)\r\n\r\nYou can create Auth0 resources non-interactively.\r\n\r\n* `auth0 apis create ...`\r\n  Creates an API (Resource Server). You can set identifier (audience), token lifetime, and offline access.\r\n\r\n* `auth0 apps create ...`\r\n  Creates an application (Native / M2M / etc).\r\n\r\n* `auth0 apps update ...`\r\n  Updates an application (callbacks, grant types, refresh token config, etc).\r\n\r\n> If you prefer the dashboard, you can ignore this section entirely.\r\n\r\n---\r\n\r\n## Troubleshooting\r\n\r\n### `grace auth login` fails and mentions refresh tokens\r\n\r\nEnsure:\r\n\r\n* the Auth0 API has **Allow Offline Access** enabled\r\n* the Auth0 Native app allows refresh tokens and is configured to issue them\r\n* `grace__auth__oidc__cli_scopes` includes `offline_access`\r\n\r\n### `grace status` returns `Unauthorized` and Grace.Server logs look empty\r\n\r\nThis usually means the CLI sent branch status requests without a usable token.\r\n\r\nCommon causes:\r\n\r\n* No interactive token is cached yet.\r\n* `GRACE_TOKEN` is not set (or is invalid).\r\n* You are expecting endpoint handler logs, but the request is rejected by authentication middleware first.\r\n\r\nQuick checks:\r\n\r\nPowerShell:\r\n\r\n```powershell\r\ngrace auth status --output Verbose\r\ngrace auth token status --output Verbose\r\ngrace status --output Verbose\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\ngrace auth status --output Verbose\r\ngrace auth token status --output Verbose\r\ngrace status --output Verbose\r\n```\r\n\r\nIf `GRACE_TOKEN` is false and interactive token is false, authenticate first:\r\n\r\nPowerShell:\r\n\r\n```powershell\r\ngrace auth login --auth device\r\n# or\r\ngrace auth login --auth pkce\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\ngrace auth login --auth device\r\n# or\r\ngrace auth login --auth pkce\r\n```\r\n\r\nIf you are using a PAT:\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$env:GRACE_TOKEN=\"grace_pat_v1_...\"\r\ngrace auth token status --output Verbose\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nexport GRACE_TOKEN=\"grace_pat_v1_...\"\r\ngrace auth token status --output Verbose\r\n```\r\n\r\nWhy logs may look empty:\r\n\r\n* `grace status` maps to `branch status`, which calls `/branch/Get` and `/branch/GetParentBranch`.\r\n* Those routes require authentication and are rejected with `401` before business handlers run.\r\n* You might see little or no endpoint-level logging for these failures.\r\n\r\nWhere to look for proof of `401`:\r\n\r\n* W3C request logs under `%TEMP%\\Grace.Server.Logs` (Windows) show request-level status codes.\r\n* Look for entries like `POST /branch/Get` and `POST /branch/GetParentBranch` with `401`.\r\n\r\nAlso note:\r\n\r\n* `--output` values are case-sensitive in current CLI behavior. Use `Verbose`, not `verbose`.\r\n* During development, TestAuth may be available. See this file for setup details:\r\n  `docs/Authentication.md` -> **Development without Auth0 (TestAuth)**.\r\n\r\n### You can see `SystemAdmin` in Cosmos, but `grace access check` says denied\r\n\r\nIf `grace auth whoami` shows your expected `GraceUserId`, but:\r\n\r\n* `grace access check --operation SystemAdmin --resource system --output Json`\r\n  returns `Denied: missing permission SystemAdmin.`,\r\n\r\nand you can also see a `SystemAdmin` assignment document in Cosmos, the most common cause is a **runtime context mismatch**.\r\n\r\nTypical mismatch causes:\r\n\r\n* The running server is using a different Orleans `serviceid` than the one that wrote the document.\r\n* You inspected a different Cosmos database or container than the running server is using.\r\n* The AccessControl grain loaded state before manual Cosmos edits (stale in-memory state until restart).\r\n\r\nWhy this happens:\r\n\r\n* System-scope role assignments are read from the AccessControl actor state keyed by scope and Orleans identity.\r\n* A document such as `grace-dev__accesscontrolactor_system` applies only to the matching Orleans service context.\r\n* If the active server context differs, authorization reads a different actor record.\r\n\r\nWhat to verify first:\r\n\r\nPowerShell:\r\n\r\n```powershell\r\ngrace auth whoami --output Json\r\ngrace access check --operation SystemAdmin --resource system --output Json\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\ngrace auth whoami --output Json\r\ngrace access check --operation SystemAdmin --resource system --output Json\r\n```\r\n\r\nThen verify server configuration:\r\n\r\n* `GRACE_SERVER_URI` points to the server you think you are testing.\r\n* The running server's Orleans `serviceid` matches the service prefix of the AccessControl actor document.\r\n* The running server's Cosmos database/container match the place where you inspected the document.\r\n\r\nRecommended recovery steps:\r\n\r\n1. Restart `Grace.Server` (or your Aspire app host) to clear any stale grain state.\r\n2. Re-run the two checks above.\r\n3. Re-open Cosmos and confirm the `system` AccessControl actor document for the active service context\r\n   contains your user principal.\r\n4. If needed, use a current `SystemAdmin` principal to grant your role again via `grace access grant-role`.\r\n\r\n### Headless environments / CI\r\n\r\nUse:\r\n\r\n* M2M auth (client credentials env vars), or\r\n* PATs (`GRACE_TOKEN`), or\r\n* `grace auth login --auth device` if interactive login is still acceptable.\r\n"
  },
  {
    "path": "docs/Branching strategy.md",
    "content": "![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n# Simplified branching strategy\n\n> Quick note: Grace doesn't have a `merge` gesture; it has a `promote` gesture. The idea is that you're not merging a changeset into a parent branch, you're saying \"The parent branch is changing its current version to point to the version in the child branch.\"\n\nGrace's default branching strategy is meant to be simple, and to help surface merge conflicts as early as possible. It's called \"single-step\". If we're right, it's all you need to successfully run a project.\n\nBranching strategy is the thing about Grace that's most different from other version control systems. Because it's so different, it's worth going over it in some detail.\n\nIn single-step branching, each child branch can be promoted only to its parent branch, and must be based on the most-recent version in the parent before being allowed to be promoted.\n\nIn other words: if I'm based on my parent branch, which means that my code includes everything in the parent branch, up to right now, then any changes in my branch are exactly equivalent to the changeset that we might imagine is being applied to the parent branch.\n\n`grace watch` helps with single-step branching by auto-rebasing your branch when a parent branch's version changes. The vast majority of the time you'll be based on the most-recent parent version without having to do anything manually, and without noticing that anything happened. (And, yes, you can turn off auto-rebase if you need to.) (..but you should try it first.)\n\nHere's a simple example:\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Green.svg)\n\nAlice and Bob are developers in the same repo, and have individual branches, named `Alice` and `Bob`, parented by `main`.\n\nWhen they do saves, checkpoints, or commits, those happen on their own branches.\n\nWhen they promote, they promote only to `main`.\n\nGrace keeps track of which version of their parent branch they're (re-)based on.\n\n```mermaid\nflowchart BT\n    Alice[Alice, based on main/ed3f4280]-->main[main/ed3f4280]\n    Bob[Bob, based on main/ed3f4280]-->main[main/ed3f4280]\n```\n\n|Branch|Current version|Based on|\n|-|-:|-:|\n|Main|`ed3f4280`|\\<root\\>|\n|Alice|`425684d8`|`ed3f4280`|\n|Bob|`9c2afa14`|`ed3f4280`||Main|ed3f4280|ed3f4280|\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Green.svg)\n\nLet's imagine that Alice makes one last update to her branch, has an updated SHA-256 value for it, and (assuming the PR is approved) promotes it to `main`.\n\n```mermaid\nflowchart BT\n    Alice[Alice, based on main/ed3f4280]-->|promoting 56d626f4|main[main/ed3f4280]\n    Bob[Bob, based on main/ed3f4280]-->main[main/ed3f4280]\n```\n\n|Branch|Current version|Based on|\n|-|-:|-:|\n|Main|`ed3f4280`|\\<root\\>|\n|Alice|`56d626f4`|`ed3f4280`|\n|Bob|`9c2afa14`|`ed3f4280`|\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Green.svg)\n\nEverything goes well, Grace completes the promotion, and `main` is updated to point to the 56d626f4 version.\n\n`Alice`, also pointing to 56d626f4, marks itself as based on it.\n\n`Bob` is no longer based on the latest version in `main`, and is therefore ineligible to promote to `main`.\n\n```mermaid\nflowchart BT\n    Alice[Alice, based on main/56d626f4]-->main[main/56d626f4]\n    Bob[Bob, based on main/ed3f4280]-->mainold[main/ed3f4280]\n```\n\n|Branch|Current version|Based on|\n|-|-:|-:|\n|Main|`56d626f4`|\\<root\\>|\n|Alice|`56d626f4`|`56d626f4`|\n|Bob|`9c2afa14`|`ed3f4280`|\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Green.svg)\n\nFortunately, Bob is running `grace watch`, so seconds later `Bob` is auto-rebased on that latest parent version in `main`.\n\nLet's assume there are no conflicts.[^conflict] The files that were updated in `main` are different that then ones updated in `Bob`. The new file versions from `main` are copied into place in the working directory.\n\nThis new version of `Bob`, which includes whatever was already changed in the branch, and whatever changed in the rebase, has a new SHA-256 value, and is automatically uploaded as a save.\n\n`Bob` is once again eligible to promote code to `main`.\n\n```mermaid\nflowchart BT\n    Alice[Alice, based on main/56d626f4]-->main[main/56d626f4]\n    Bob[Bob, now based on main/56d626f4]<-->|rebase on 56d626f4|main[main/56d626f4]\n```\n\n|Branch|Current version|Based on|\n|-|-:|-:|\n|Main|`56d626f4`|\\<root\\>|\n|Alice|`56d626f4`|`56d626f4`|\n|Bob|`21519a1b`|`56d626f4`|\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Green.svg)\n\n[^conflict]: If there are conflicts, Grace will have a native conflict-resolution UI, as well as a way to navigate it through the CLI.\n"
  },
  {
    "path": "docs/Continuous review.md",
    "content": "# Continuous review\n\nContinuous review tracks promotion-set candidates, evaluates gates, and records\nreview artifacts in a deterministic and auditable flow.\n\n## Canonical workflow model\n\n- Use `grace queue ...` for promotion-set queue operations.\n- Use `grace candidate ...` for candidate-first reviewer operations:\n  `get`, `required-actions`, `attestations`, `retry`, `cancel`, `gate rerun`.\n- Use `grace review ...` for promotion-set scoped reviewer actions:\n  `open`, `checkpoint`, `resolve`.\n- Use `grace review report ...` for candidate-scoped report output:\n  `show`, `export`.\n\n## Current API surface\n\n### Queue endpoints\n\n- `POST /queue/status`\n- `POST /queue/enqueue`\n- `POST /queue/pause`\n- `POST /queue/resume`\n- `POST /queue/dequeue`\n\n### Candidate and report endpoints\n\n- `POST /review/candidate/get`\n- `POST /review/candidate/retry`\n- `POST /review/candidate/cancel`\n- `POST /review/candidate/required-actions`\n- `POST /review/candidate/attestations`\n- `POST /review/candidate/gate-rerun`\n- `POST /review/report/get`\n\n### Review endpoints\n\n- `POST /review/notes`\n- `POST /review/checkpoint`\n- `POST /review/resolve`\n- `POST /review/deepen` (stub)\n\n### Policy endpoints\n\n- `POST /policy/current`\n- `POST /policy/acknowledge`\n\n## CLI examples\n\n### Queue management\n\nPowerShell:\n\n```powershell\n./grace queue enqueue `\n  --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000 `\n  --branch main `\n  --policy-snapshot-id e3b0c44298fc1c149afbf4c8996fb924 `\n  --work 42\n\n./grace queue status --branch main\n./grace queue pause --branch main\n./grace queue resume --branch main\n./grace queue dequeue --branch main --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000\n```\n\nbash / zsh:\n\n```bash\n./grace queue enqueue \\\n  --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000 \\\n  --branch main \\\n  --policy-snapshot-id e3b0c44298fc1c149afbf4c8996fb924 \\\n  --work 42\n\n./grace queue status --branch main\n./grace queue pause --branch main\n./grace queue resume --branch main\n./grace queue dequeue --branch main --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000\n```\n\n`--work` accepts either a GUID `WorkItemId` or a numeric `WorkItemNumber`.\n\n### Candidate-first operations\n\nPowerShell:\n\n```powershell\n./grace candidate get --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate required-actions --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate attestations --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate retry --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate gate rerun --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c --gate policy\n```\n\nbash / zsh:\n\n```bash\n./grace candidate get --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate required-actions --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate attestations --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate retry --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n./grace candidate gate rerun --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c --gate policy\n```\n\n### Promotion-set review actions\n\nPowerShell:\n\n```powershell\n./grace review open --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000\n\n./grace review checkpoint `\n  --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000 `\n  --reference-id f12a0d31-0d5a-4a5f-a5a7-3d2c3a9f5b2c\n\n./grace review resolve `\n  --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000 `\n  --finding-id 6e58b4de-7f3b-4a2b-9a6f-111111111111 `\n  --approve `\n  --note \"Reviewed and acceptable.\"\n```\n\nbash / zsh:\n\n```bash\n./grace review open --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000\n\n./grace review checkpoint \\\n  --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000 \\\n  --reference-id f12a0d31-0d5a-4a5f-a5a7-3d2c3a9f5b2c\n\n./grace review resolve \\\n  --promotion-set 3d5c4d9a-0123-4567-89ab-987654321000 \\\n  --finding-id 6e58b4de-7f3b-4a2b-9a6f-111111111111 \\\n  --approve \\\n  --note \"Reviewed and acceptable.\"\n```\n\n### Report-first review output\n\nPowerShell:\n\n```powershell\n./grace review report show --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n\n./grace review report export `\n  --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c `\n  --format markdown `\n  --output-file .\\review-report.md\n```\n\nbash / zsh:\n\n```bash\n./grace review report show --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c\n\n./grace review report export \\\n  --candidate 4fe3c2a9-35f0-4dc2-9f8b-4d3e2f1a0b9c \\\n  --format markdown \\\n  --output-file ./review-report.md\n```\n\n### Source attribution in history\n\nUse the global `--source` option to tag and filter automation activity.\n\nPowerShell:\n\n```powershell\n./grace history show --source codex\n./grace history search workitem --source codex\n```\n\nbash / zsh:\n\n```bash\n./grace history show --source codex\n./grace history search workitem --source codex\n```\n\nYou can also set the environment fallback:\n\nPowerShell:\n\n```powershell\n$env:GRACE_SOURCE=\"codex\"\n./grace history show\n```\n\nbash / zsh:\n\n```bash\nexport GRACE_SOURCE=\"codex\"\n./grace history show\n```\n\n## Current limitations and stubs\n\n- `grace review inbox` is still a CLI stub.\n- `grace review deepen` is still a CLI and server stub.\n- Queue processing and automatic candidate state transitions are not fully\n  orchestrated server-side; external automation is expected.\n- Gate implementations are still partial for some gate types.\n"
  },
  {
    "path": "docs/Data types in Grace.md",
    "content": "# Data types in Grace\n\nGrace uses a fairly simple data structure to keep track of everything. It's more robust than Git's, for sure, but it's as simple as I could make it.\n\nIn this document, first, you'll find an Entity Relationship Diagram (ERD) showing the most relevant types.\n\nAfter the diagram, you'll find descriptions of each data type. You can skip directly to the data type you're interested in by clicking the corresponding link below:\n\n- [Owner and Organization](#owner-and-organization-ie-multitenancy)\n- [Repository](#repository)\n- [Branch](#branch)\n- [DirectoryVersion](#directoryversion)\n- [Reference](#reference)\n- [FileVersion](#fileversion)\n\nI'm sure the types will evolve a bit as we move towards a 1.0 release, but the overall structure should be stable now.\n\nAfter those descriptions, at the bottom of this document, you'll find a [detailed entity relationship diagram](#detailed-entity-relationship-diagram). This ERD is incomplete, and there are, of course, many other data types in Grace. It's meant to illustrate the most interesting parts, to help you understand the structure of a repository and its contents. Please refer to it as you read the explanations of each type.\n\n## Entity Relationship Diagram\n\nThe diagram below shows the most important data types in Grace, and how they relate to each other. A [more-detailed ERD](#detailed-entity-relationship-diagram) is available at the bottom of this document.\n\n```mermaid\nerDiagram\n    Owner ||--|{ Organization : \"has 1:N\"\n    Organization ||--|{ Repository : \"has 1:N\"\n    Repository ||--|{ Branch : \"has 1:N\"\n    Branch ||--|{ Reference : \"has 0:N\"\n    Repository ||--|{ DirectoryVersion : \"has 1:N\"\n    Reference ||--|| DirectoryVersion : \"refers to exactly 1\"\n    DirectoryVersion ||--|{ FileVersion : \"has 0:N\"\n```\n\n## Owner and Organization; i.e. Multitenancy\n\nGrace has a lightweight form of multitenancy built-in. This structure is meant to help large version control hosting platforms to integrate Grace with their existing customer and identity systems.\n\nI've specifically chosen to do have a two-level Owner / Organization structure based on my experience at GitHub. GitHub started with the construct of an Organization, and in recent years has been adding an \"Enterprise\" construct above Organizations, to allow large companies to have multiple Organizations managed under one structure. Seeing the importance of that feature set to large companies made it an easy decision to just start with a two-level structure.\n\nIt's not my intention for Grace to replace the identity / organization system for any hoster, and that's why there really isn't much in these data types. They're meant to be \"hooks\" that a hoster can refer to from their identity systems so they can implement whatever management features they need to safely serve Grace repositories.\n\nOwner and Organization are the least-used of the data types here. They get created relatively infrequently, they get updated even less frequently, and they get deleted not much at all.\n\n### What about personal accounts?\n\nFor individual users - like personal user accounts on GitHub that don't belong to any organization - Grace will have one Owner and one Organization that is just for that user, and all user-owned repositories would sit under that Organization.\n\nThere's nothing stopping an individual user from having multiple Organizations (unless the hoster prevents it). There's no performance difference either way.\n\n## Repository\n\nNow we get to the version control part.\n\nRepository is where Grace keeps settings that apply to the entire repository, that apply to each branch by default, and that apply to References and DirectoryVersions in the repository.\n\nSome examples:\n\n- RepositoryType - Is the repository public or private?\n- SearchVisibility - Should the contents of this repository be visible in search?\n- Timings for deleting various entities -\n  - LogicalDeleteDays - How long should a deleted object be kept before being physically deleted?\n  - SaveDays - How long should Save References be kept?\n  - CheckpointDays - How long should Checkpoint References be kept?\n  - DirectoryVersionCacheDays - How long should the memoized contents of the entire directory tree under a DirectoryVersion be kept?\n  - DiffCacheDays - How long should the memoized results of a Diff between two DirectoryVersions be kept?\n- RecordSaves - Should Auto-save be turned on for this repository?\n\nIn general, once a Repository is created and the settings adjusted to taste, the Repository record will be updated very infrequently.\n\n## Branch\n\nBranch is where branches in a repository are defined. It just holds settings that apply to the Branch.\n\nThe most important settings there are:\n\n- ParentBranchId - Which branch is the parent of this branch?\n- \\<_Reference_\\>Enabled - These control which kinds of References are allowed on the Branch\n  - PromotionEnabled\n  - CommitEnabled\n  - CheckpointEnabled\n  - SaveEnabled\n  - TagEnabled\n  - ExternalEnabled\n\nI'm sure there will be more settings here as we get to v1.0.\n\nBranches are created and deleted frequently, of course, but they're updated pretty infrequently.\n\nThat might seem weird if you're used to Git. In Grace, when you do things like `grace checkpoint` or `grace commit` you're not updating the status of a Branch; you're creating a new Reference _in_ that branch. Nothing in the Branch itself changes.\n\n## DirectoryVersion\n\nDirectoryVersion holds the data for a specific version of a directory anywhere in a repo. Every time a file in a directory changes, a new DirectoryVersion is created that holds the new state of the directory. If the contents of a subdirectory change, that directory will get a new DirectoryVersion, and so will the next directory up the tree, until we reach the root of the repository.\n\nIn other words, DirectoryVersion is how we capture each unique state in a repository.\n\nOne interesting thing here is that, like the other entities here, Grace uses a Guid for the primary key DirectoryVersionId, and does not use the Sha256Hash as the unique key (even though it always will be unique). My reason for choosing to have an artificial key instead of just using the Sha256Hash is the challenge that Git has had, and is having, migrating to SHA-256, given how deeply embedded SHA-1 is in the naming of objects in Git. It seems best to keep Sha256Hash as a data field, and not as a key, to make it easier to change the hash algorithm in the future.\n\nAlso, DirectoryVersion has the RepositoryId it belongs to, but does not keep a BranchId. This is because a unique version of the Repository, i.e. a DirectoryVersion, can be pointed to from multiple References and from multiple Branches.\n\nSo, DirectoryVersion contains:\n\n- DirectoryVersionId - This is a Guid that uniquely identifies each DirectoryVersion.\n- RepositoryId - not BranchId\n- Sha256Hash - Computed over the contents of the directory; the algorithms for computing the Sha256Hash of a [file](https://github.com/ScottArbeit/Grace/blob/337ed395b7f5d033ceb9d178b4fd9442fa383ee5/src/Grace.Shared/Services.Shared.fs#L53) and a [directory](https://github.com/ScottArbeit/Grace/blob/337ed395b7f5d033ceb9d178b4fd9442fa383ee5/src/Grace.Shared/Services.Shared.fs#L92) are in [Services.Shared.fs](https://github.com/ScottArbeit/Grace/blob/main/src/Grace.Shared/Services.Shared.fs).\n- RelativePath - no leading '/'; for instance `src/foo/bar.fs`\n- Directories - a list of DirectoryVersionId's that refer to the sub-DirectoryVersions.\n- Files - a list of FileVersions, one for each not-ignored file in the directory\n- Size - int64\n\nDirectoryVersions are created and deleted frequently, as References are created and deleted.\n\n### RootDirectoryVersion\n\nBecause it's such an important construct, in Grace's code you'll see `RootDirectoryVersion` a lot. This is a DirectoryVersion with the path '.', which is the [definition of \"root directory\"](https://github.com/ScottArbeit/Grace/blob/337ed395b7f5d033ceb9d178b4fd9442fa383ee5/src/Grace.Shared/Constants.Shared.fs#L173-L174) in Grace. Because the RootDirectoryVersion sits at the top of the directory tree, we point to it in a Reference, rather than any sub-DirectoryVersion, as representing a unique version of the repository.\n\n## Reference\n\nIn Grace, a Reference is how we mark specific RootDirectoryVersions as being interesting in one way or another.\n\nReferences have a ReferenceType that indicates what kind it is, so there's no such thing as a Commit entity or a Save entity. They're all just References.\n\nThe interesting parts of a Reference are:\n\n- ReferenceId - This is a Guid that uniquely identifies each Reference.\n- BranchId - The Branch that this Reference is in. A Reference can only be in one Branch.\n- DirectoryVersionId - The RootDirectoryVersion that this Reference points to.\n- Sha256Hash - The Sha256Hash of the DirectoryVersionId that this Reference points to. Denormalized here for performance reasons.\n- ReferenceType - What kind of Reference is this?\n  - Promotion - This is a Reference that was created by promoting a Commit reference from a child branch to this branch.\n  - Commit - Commits are candidates for promotion.\n  - Checkpoint - This is for you to mark a specific version of the repository as being interesting to you. In Git, this is what you'd think of as an intermediate commit as you complete your work.\n  - Save - These are automatically created by Grace on every save-on-disk, if Auto-Save is turned on.\n  - Tag - This is a Reference that was created by tagging a Reference.\n  - External - This is a Reference that was created by an external system, like a CI system.\n  - Rebase - This is the Reference that gets created when a branch is Rebased on the latest Promotion in its parent branch\n- ReferenceType - The attached to the Reference.\n- Links - This is a way to link this Reference to another in some relationship.\n\nReferences and DirectoryVersions are where the action happens. New References and DirectoryVersions are being created with every save-on-disk (if you have Auto-Save turned on, which you should), and with every checkpoint / commit / promote / tag / external.\n\nThe ratio of new-DirectoryVersions-to-new-References is directly proportional to how deep in the directory tree the updated files are. For every directory level, a new DirectoryVersion will be created. For example, if I update a file called `src/web/js/lib/blah.js` and hit save, that will create one Save Reference, and five new DirectoryVersions - one for the root, and one each for each directory in the path.\n\nSaves have short lifetimes, and checkpoints (by default) have longer, but finite, lifetimes, and they both get deleted at some point. Any DirectoryVersions that are unique to those references, and any FileVersions in object storage that only appear in those references, get deleted when the Reference is deleted.\n\nAlso, of course, every time a Branch is deleted, all References in that Branch get deleted. And all DirectoryVersions unique to those References get deleted. Etc.\n\nIt's completely normal in Grace for References to be deleted. Happens all the time.\n\n## FileVersion\n\nThe FileVersion contains the metadata for a file in a DirectoryVersion. It's the metadata for the file, not the file itself.\n\nThe file itself is stored in object storage, and the FileVersion has a BlobUri that points to it.\n\nThe interesting parts of a FileVersion are:\n\n- RepositoryId - The Repository that this FileVersion is in.\n- RelativePath - The path of the file, relative to the Repository root.\n- Sha256Hash - The Sha256Hash of the file.\n- IsBinary - Is the file binary?\n- Size - The size of the file (int64).\n- BlobUri - The URI of the file in object storage.\n\n## Detailed Entity Relationship Diagram\n\nThe diagram below shows the most important data types in Grace, and how they relate to each other. Not every field in each data type is shown - feel free to check out [Types.Shared.fs](https://github.com/ScottArbeit/Grace/blob/main/src/Grace.Shared/Types.Shared.fs) and [Dto.Shared.fs](https://github.com/ScottArbeit/Grace/blob/main/src/Grace.Shared/Dto/Dto.Shared.fs) to see the full data types - but this should give you a good idea of how the data is structured.\n\n```mermaid\nerDiagram\n    Owner ||--|{ Organization : \"has 1:N\"\n    Owner {\n        OwnerId Guid\n        OwnerName string\n        OwnerType OwnerType\n        SearchVisibility SearchVisibility\n    }\n    Organization ||--|{ Repository : \"has 1:N\"\n    Organization {\n        OrganizationId Guid\n        OrganizationName string\n        OwnerId Guid\n        OrganizationType OrganizationType\n        SearchVisibility SearchVisibility\n    }\n    Repository ||--|{ Branch : \"has 1:N\"\n    Repository {\n        RepositoryId Guid\n        RepositoryName string\n        OwnerId Guid\n        OrganizationId Guid\n        RepositoryType RepositoryType\n        RepositoryStatus RepositoryStatus\n        DefaultServerApiVersion string\n        DefaultBranchName string\n        LogicalDeleteDays double\n        SaveDays double\n        CheckpointDays double\n        DirectoryVersionCacheDays double\n        DiffCacheDays double\n        Description string\n        RecordSaves bool\n    }\n    Branch ||--|{ Reference : \"has 1:N\"\n    Branch {\n        BranchId Guid\n        BranchName string\n        OwnerId Guid\n        OrganizationId Guid\n        RepositoryId Guid\n        UserId Guid\n        PromotionEnabled bool\n        CommitEnabled bool\n        CheckpointEnabled bool\n        SaveEnabled bool\n        TagEnabled bool\n        ExternalEnabled bool\n        AutoRebaseEnabled bool\n    }\n    Repository ||--|{ DirectoryVersion : \"has 1:N\"\n    Reference ||--|| DirectoryVersion : \"refers to exactly 1\"\n    Reference {\n        ReferenceId Guid\n        DirectoryVersionId Guid\n        Sha256Hash string\n        ReferenceType ReferenceType\n        ReferenceTest string\n        Links ReferenceLinkType[]\n    }\n    DirectoryVersion {\n        DirectoryVersionId Guid\n        RepositoryId Guid\n        RelativePath string\n        Sha256Hash string\n        Directories DirectoryVersionId[]\n        Files FileVersion[]\n    }\n    DirectoryVersion ||--|{ FileVersion : \"has 1:N\"\n    FileVersion {\n        RepositoryId Guid\n        RelativePath string\n        Sha256Hash string\n        IsBinary bool\n        Size int64\n        BlobUri string\n    }\n```\n"
  },
  {
    "path": "docs/Design and Motivations.md",
    "content": "# Design and Motivations\n\nHi, I'm Scott. I created Grace.\n\nI'll use first-person singular in this document because I want to share what the early design and technology decisions were for Grace, and why I started writing it in the first place. I'll happily rewrite it in first-person plural when it's appropriate.\n\nFor shorter answers to some of these, please see [Frequently Asked Questions](Frequently%20asked%20questions.md).\n\n---\n\n## Table of Contents\n\n[A word about Git](#a-word-about-git)\n\n[User experience is everything](#user-experience-is-everything)\n\n[The origin of Grace](#the-origin-of-grace)\n\n[Perceived performance](#perceived-performance)\n\n[CLI + Native GUI + Web UI + Web API](#cli--native-gui--web-ui--web-api)\n\n[F# and Functional programming](#f-and-functional-programming)\n\n[Source control isn't systems-level](#source-control-isnt-systems-level)\n\n[Cloud-native version control](#cloud-native-version-control)\n\n[Why Grace is centralized](#why-grace-is-centralized)\n\n[Performance; or, Isn't centralized version control slower?](#performance-or-isnt-centralized-version-control-slower)\n\n[How much Git should we keep?](#how-much-git-should-we-keep)\n\n[Scalability](#scalability)\n\n[Monorepos](#monorepos)\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## A word about Git\n\nIt's not possible to design a version control system (VCS) today without designing something that relates to, interfaces with, and/or somehow _just reacts to_ Git. In order to explain some of the choices I've made in Grace, I _have_ to mention Git. Mostly, of course, I'll do that if I think Grace is better in some way or other.\n\nWith that said, and just to be clear... I respect Git enormously. It will take years for any new VCS to approximate the feature-set of Git. Until a new one starts to gain momentum and gets a sustained programming effort behind it – open-source and community-supported – every new VCS will sort-of be a sketch compared to everything that Git can do.\n\nThe maintainers of Git are among the best programmers in the world. The way they continue to improve Git's scalability and performance, year-after-year, while maintaining compatibility with existing repositories, is an example of how to do world-impacting programming with skill and, dare I say, grace.\n\nGit has been around for 17 years now, and it's not disappearing anytime soon. If you love Git, if it fits your needs well, I'm guessing you will be able to continue to use it for the next 15-20 years without a problem. (What source control might look like in 2042 is anyone's guess.)\n\n### Git is dominant now, but...\n\nWhether Git will remain the dominant version control system for that entire time is quite another question. I believe that _something else_ will capture people's imagination enough to get them to switch away from Git at some point. My guess about when that will happen is: soon-ish. Like, _something else_ is being created now-ish, \\<waves hands\\>±2 years. There are some wonderful source control projects going on right now that are exploring this space. I offer Grace in the hope that _it_ will be good enough to make people switch. Time will tell.\n\nGit is amazing at what it does. I'm openly borrowing from Git where I think it's important to (ephemeral working directory, lightweight branching, SHA-256 hashes, and so much else).\n\nI just think that it's a different time now. The constraints that existed in 2005 in terms of hardware and networking, the constraints that Git was designed to fit in, don't hold anymore. We can take advantage of current client and server and cloud capabilities to design something really different, and even better.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## User experience is everything\n\nNow that I've said nice things about Git....\n\n### Git's UX is terrible.\n\n [I](https://xkcd.com/1597/) [hope](https://gracevcsdevelopment.blob.core.windows.net/static/RandomGitCommands.jpeg) [this](https://git-man-page-generator.lokaltog.net) [is](https://rakhim.org/honestly-undefined/13/) [not](https://gracevcsdevelopment.blob.core.windows.net/static/MemorizingSixGitCommands.jpg) [a](https://www.quora.com/Why-is-Git-so-hard-to-learn) [controversial](https://www.quora.com/If-I-think-Git-is-too-hard-to-learn-does-it-mean-that-I-dont-have-the-potential-to-be-a-developer) [statement](https://twitter.com/markrussinovich/status/1395143648191279105). And [I](https://twitter.com/robertskmiles/status/1431560311086137353) **[know](https://twitter.com/markrussinovich/status/1578451245249052672)** [I'm](https://ohshitgit.com/) [not](https://twitter.com/dvd848/status/1508528441519484931) [alone](https://twitter.com/shanselman/status/1102296651081760768) [in](https://www.linuxjournal.com/content/terrible-ideas-git) [thinking](https://blog.acolyer.org/2016/10/24/whats-wrong-with-git-a-conceptual-design-analysis/) [it](https://matt-rickard.com/the-terrible-ux-of-git).\n\nLearning Git is far too hard. It's basically a hazing ritual that we put ourselves through as an industry. Git forces the user to understand far too much about its internals just to become a proficient user. Maybe 15%-20% of users really understand it.\n\nMany of its regular users are literally afraid of it. Including me.\n\n> Bad software, designed without empathy, that restricts people to only follow strict procedures to achieve one specific goal, has trained millions of people that software is cumbersome, inflexible, and even hostile and that users have to adapt to the machine, if they want to get anything done. The future of computing should be very much the opposite. Good software can augment the human experience by becoming the tool that’s needed in the moment, unrestricted by limitations in the physical world. It can become the personal dynamic medium through which exploring and expressing our ideas should become simpler rather than more difficult. [^StefanLesser]\n> \n> \\- Stefan Lesser\n\n### Grace's UX is focused on simplicity\n\nGrace is explicitly designed to be easy to use, and easy to understand.\n\nIt's as simple as possible. It has abstractions. Users don't have to know the details of how it works to use it.\n\nBecause of that, Grace has fewer concepts for users to understand to become and feel proficient in using it.\n\nGrace formats output to make it as easy to read as possible, and also offers JSON output, minimal output, and silent output for each command. [^output]\n\nAnd in a world where hybrid and remote work is growing, Grace offers entirely new experiences with a live, two-way channel between client and server, linking repository users together in new ways, including auto-rebasing immediately after promotions (which are merges, sort-of).\n\nThere's so much more to do in UX for version control. Grace is a platform for exploring where it can go next.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## The origin of Grace\n\nThere was an informal Source Control Summit in November, 2020 that I had the opportunity to attend, and I had the chance to have some additional side conversations with a few of the other attendees.\n\nThe vibe I got from those interactions – and I want to emphasize that this was _my_ takeaway, and that I do not speak for anyone else – was that 1) we're all still just mining for incremental improvements in Git; 2) we're getting tired of Git and whatever else we're using; and 3) we're not sure what could come next that would change that.\n\nThat led me to sitting outside on my front porch in December, 2020 – still in pandemic lockdown, in the darkest month of a dark year – and starting to think about what **I** would want in a version control system.\n\nIt all started that first night with a few themes:\n\n- It had to be easy-to-use. The pain of learning Git, and the continuing fear of it, has always been a sore spot for me, and, I know, for millions of others.\n- It had to be cloud-native, so it could take advantage of the fast, cloud-scale computing that we're all used to in almost every other kind of software, and get away from using file servers.\n- It had to have live synchronization between client and server – I was thinking of the OneDrive sync client as a good example – as the basis for being able to build important new features.\n- It had to fundamentally break away from Git. No \"Git client but a different backend\". No \"New client, but Git for the storage layer.\" No \"it should speak Git protocol\".\n\nI just wanted to start with a blank sheet of paper, keep the things about Git that we all like, take advantage of modern cloud-native services, and get rid of the complexity.\n\nGrace is the version control system that I'd want to use.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Perceived performance\n\nMeasuring actual performance of any significant system is important, and Grace will have performance benchmarks that can be run alongside other kinds of tests to ensure that regressions don't sneak in. I care deeply about performance.\n\nWhat I care even more about is _perceived performance_. What I mean by that is something more subjective than just \"how many milliseconds?\" I mean: **does it _feel_ fast**?\n\nPerceived performance includes not just how long something takes, but how long it takes relative to other things that a user is doing, and how consistent that time is from one command invocation to the next, to the next, to the next.\n\nMy experience is that running _fast enough_, _consistently_, is what gives the user the feeling that a system is fast and responsive.\n\nThat's what Grace is aiming for: both _fast_, and _consistent_. Fast + consistent means that users can develop expectations and muscle memory when using a command, and that running a command won't take them out of their flow.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## CLI + Native GUI + Web UI + Web API\n\nAnother avenue for providing great UX is in providing choices about how to interact with Grace.\n\n### CLI\n\nObviously, Grace starts with a command-line interface (CLI). I've designed it for ease-of-use. As much as possible, default values are used to save typing. By default, most output is shown in tables with borders, and is formatted to help guide your eyes to the important content. Grace can also provide output in minimal text, JSON, verbose, or no output at all (silent).\n\n### Native GUI\n\nWait, what? No one does those anymore.\n\nYeah, well... the thing is, I really don't like Electron apps. Like, at all.\n\nIt's simply not possible to recreate the snappiness, the stick-to-your-finger-ness, the _certainty_ that the UI is reacting to you, that you get in a native app when you're writing in a browser. It's just not. I've been watching this for years now, and almost no one even tries.\n\n\"What about Visual Studio Code?\" I hear someone say. It's among the best examples, for sure. But I don't _love_ it. Look how much programming and how many person-years have gone into it to make it OK. Look at the way they had to completely rewrite the terminal window output using Canvas because nothing else in a browser was fast enough.\n\nI don't see a lot of other Electron apps getting nearly that level of effort and programming. And they're all just... not great.\n\nI fear that, as an industry, we're failing our fellow human beings, and we're failing each other, by accepting second-rate UX on the first-rate hardware we have.\n\nWe're making this choice for one reason: our own convenience as programmers. We're prioritizing developer ergonomics over user experience, and claiming that it's for business reasons. And we're usually not making great UX with it.\n\nWe have tools today that can create native apps on multiple platforms from a single codebase, and that's what I'm taking advantage of. There's nothing in Grace's UX that I'm currently imagining that requires any complex user controls that can't be rendered in any of those tools... you know: text blocks, lines, borders, normal input fields and buttons / checkboxes / radio buttons. Maybe a tree view or some graphs if I'm feeling fancy.\n\nWe can provide incredible experiences when we take advantage of the hardware directly, and I intend to.\n\n### Web UI\n\nSo, after all that... I'm creating a Web UI? What gives?\n\nBrowsers are great for browsing and light functionality, and that's all Grace will need.\n\n### Web API\n\nGrace Server itself is simply a modern, 2024-style Web API. If there's something you'd rather automate by calling the server directly, party on. Grace ships with a .NET SDK (because that's what the CLI + Native GUI + Web UI use), and that SDK is simply a projection of the Web API into a specific platform. It should be trivial to create similar SDK's for other languages and platforms.\n\nIt's about choices for the user. It's about understanding that sometimes the best way to share something is with a URL. And it's about providing a place that we can collaborate on what the default Grace's UI should look like.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## F# and functional programming\n\n### Grace is written primarily in F\\#\n\nThe main reason for this is simple: **F# is my favorite programming language right now**. It's beautiful, and it feels lightweight, but it's really strongly-typed and very fast.\n\nBut... there are other reasons.\n\n### Reconsidering object-oriented programming\n\nLike many of my peers, I've been specializing in object-oriented (OO) code for a long time. For me, it was C++ starting in 1998, and then .NET starting with .NET Framework Beta 2 in June, 2001. I've written tens-of-thousands of lines of object-oriented code. In 2015, I started to learn Category Theory, and in 2017-18, I had the opportunity to work on a project at Microsoft Research that was written in F#. I went back to C# for a bit after that, but, the seed was planted, and in a small 2020 pandemic side project, I decided to use F# to _really_ learn to think functionally and put a little bit of that Category Theory to use.\n\nAfter 20+ years of writing OO code, I've come to the conclusion, as have others, that we've hit a ceiling in quality and maintainability in object-oriented-ish code for anything beyond a medium-sized codebase. We've adopted many practices to cover up for the problems with OO, like dependency injection and automated unit testing so we can refactor safely, but the truth is that without a significant investment in Developer Experience, and sustained effort to just keep the code clean, many large OO projects become supremely brittle and hard to maintain.\n\nYou may disagree, and that's fine. There's a fair argument that when you design OO systems as message-passing systems (and Grace does this using the Actor pattern) they factor really well. I'm not saying it's not possible to have a large and still-flexible OO codebase, just that it's rare and takes deliberate effort to keep it that way.\n\nFunctional programming offers a new path to create large-scale codebases that sidestep these problems. No doubt, over the coming years, as more teams try functional code, we'll find anti-patterns that we need to deal with (and, no doubt, I have some of them in Grace), but having personally taken the mindset journey from OO to functional, my field report is: we'll benefit greatly as an industry if we take a more functional and declarative approach to coding. It can do wonders everywhere, not just in the UI frameworks where we've already seen the benefits.\n\nWhether you choose Haskell, Scala, F#, Crystal, or some other functional language, I invite you to try functional programming. It's a journey, for sure, but it's so worth it. Not only will you learn a new way to think about organizing code, you'll become a better OO programmer for it.\n\n### .NET is really nice to use, and well-supported\n\nFor those who haven't worked with it yet... .NET is great now. Really. Let me explain why.\n\nThe old days of .NET being a Windows-only framework are long-since over. .NET is fully cross-platform, suporting Windows, Linux, MacOS, Android, and iOS. It's the most well-loved framework according to [Stack Overflow's 2022 Developer Survey](https://survey.stackoverflow.co/2022/#section-most-popular-technologies-other-frameworks-and-libraries), as it was in [2021](https://insights.stackoverflow.com/survey/2021#section-most-popular-technologies-other-frameworks-and-libraries), and Microsoft has continued to pour work into making it faster, better, easier-to-use, and well-documented. NuGet, .NET's package manager, has community-supported packages for almost every technology one might wish to interface with.\n\nIn terms of performance, .NET has been near the top of the [Techempower Benchmarks](https://www.techempower.com/benchmarks/#section=data-r21&test=composite) for years, and the .NET team and community continue to find performance improvements in every release.\n\nAs far as developer experience, .NET is just a really nice place to spend time. The documentation is amazing, the examples and StackOverflow support are first-rate.\n\nThe .NET team has written a wonderful blog post called [What is .NET, and why should you choose it?](https://devblogs.microsoft.com/dotnet/why-dotnet/), the first of a series diving into the design of the platform.\n\nIs it perfect? No, of course not. Nothing in our business is.\n\nIs it \"really good\" or \"great\" a lot of the time? Does it continue to improve release after release? In my experience... yes, absolutely.\n\nWill it be supported for a long time? .NET has great adoption in both open-source and enterprise shops. Unity, one of the most popular game engines, is written in C#. Microsoft itself runs many of its insanely large first-party Azure services on .NET, and that alone will keep .NET around and on a continuous improvement cycle for the forseeable future.\n\nSo, it's very fast, it has great corporate and community support, it runs on every major platform, and it's loved by those who use it. I'm not saying that other tech stacks aren't great, just that .NET is great now and well worth a long-term bet.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Source control isn't systems-level\n\nI like things that go fast. My second programming language – at age 11 – was 6502 Assembler. I've written and read code in IBM 370 Assembler and 80x86 Assembler. I've written C and C++, and continue to pay attention to the wonderful work being led by [Herb Sutter](https://www.youtube.com/user/CppCon/search?query=herb%20sutter) and [Bjarne Stroustrup](https://www.youtube.com/user/CppCon/search?query=bjarne) to make C++ faster, safer, less verbose, and easier to use. I applaud the work by Mozilla and the Rust community to explore the space of safer, very fast systems programming. I consider any public talk by [Chandler Carruth](https://www.youtube.com/results?search_query=chandler+carruth) to be mandatory viewing.\n\nI'm aware of what it means to be coding down-to-the-metal. I grew up on it, and still try to think in terms of hardware capabilities, even as I use higher-level frameworks like .NET.\n\nWith that said, the idea that version control systems have to be written in a systems-level language, just because they all used to be, isn't true, especially for a centralized VCS that's just a modern Web API and its clients.\n\nGrace relies on external databases and object storage services, and so there's not much Git-style byte-level file manipulation. Given how fast .NET is (within 1% of native C++ when well-written), and the fact that network round-trips are involved in most things that Grace does, it's not likely that writing Grace in C++ or Rust would make a difference in perceived performance for users. A lot of the clock time during commands is spent on those network round-trips, even on the server. Using F# and .NET for the computation – i.e. for Grace itself – is more than fast enough compared to all of that.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Cloud-native version control\n\nI've personally installed and maintained hundreds of servers and virtual machines in my career. I racked dozens of them myself. It seemed fun at the time. I'm over it.\n\nThat's why I'm a huge fan of Platform-as-a-Service (PaaS), and why Grace was imagined on its first day as a cloud-native system. I starting tracking the [Dapr project](https://dapr.io) as soon as it was announced, and saw it as a perfect solution for being able to write a cloud-native, PaaS-based system, while allowing everyone to choose their own deployment adventure.\n\n### Your choice of services at deployment time\n\nGrace runs on Dapr to allow you to choose which PaaS or cloud or even on-premises services it runs on.\n\nThe database, the observability platform, the service bus / event hub pieces, and more, will be configurable by you. Grace will run on anything Dapr supports.\n\n### Object storage is different\n\nGrace uses an object storage service to save each version of the files in a repo (i.e. Azure Blob Storage, AWS S3, Google Cloud Storage, etc.). Although Dapr does support pluggable object storage providers, using Dapr for Grace's object storage isn't appropriate for Dapr's design.\n\nDapr is perfect for using object storage for storing smaller blobs, and although most code files fall in the size range that works well for Grace, I want Grace to support virtually unlimited file sizes. That means that it's best for Grace to directly use the specific API's for the storage providers, and to allow the CLI / client to communicate directly with the object storage service, offloading that work to the service where it belongs.\n\n#### A note about the actual current state of Grace\n\nThus far, Grace has been written only to run on Microsoft Azure. (It's the cloud provider I know best.)\n\nThere was an issue with Dapr when I started writing Grace that caused me to \"work around\" Dapr's support for databases. It has since been fixed – the ability to query actor storage using a Dapr-specific syntax – and I intend to remove the Azure Cosmos DB code I wrote in favor of that Dapr code over the coming months, enabling Grace to run not just on Cosmos DB, but on any data service that Dapr supports for actor storage.\n\nAs mentioned above, the best thing for Grace is to directly use the specific API's of the object storage providers in the client. To do that securely, at a minimum, the object storage provider must support the concept of a time-limited and scope-limited token that can be generated at the server to be handed to the client for directly accessing the object storage service. (For example, Azure Blob Storage has [Secure Access Signatures](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview).)\n\nAlthough I've only implemented support for Azure Blob Storage so far, I've created some light structure in the code using discriminated unions to try to keep me honest and able to implement support for other object storage services without too much difficulty.\n\n### How does Dapr affect performance?\n\nThe simple version is: it adds ~1ms per request through Dapr, when we ask Dapr's Actor Placement Service (running in a separate container) which Grace application instance will have the specific Actor we're interested in. It's negligable compared to overall network round-trip between client and server, and well worth it for the ease-of-use of the Actor pattern in Dapr. To make that even faster, I use a short-term in-memory cache on each application instance for specific data to save even on those calls.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Why Grace is centralized\n\nGrace is a centralized version control system (CVCS). To be clear, there are valid reasons to use a distibuted version control system (DVCS), like Git. There are other new DVCS projects underway, and there are some great ideas in them. Grace is clearly not well-suited for a situation where a DVCS is required, and that's OK.\n\nI wanted to take a different approach with Grace, because:\n\n- by removing the complexity of being distributed, Grace's command surface can be much simpler to use and understand\n- as long as Grace is _fast enough_ (see [below](#performance-or-isnt-centralized-version-control-slower)), and easy to use, most users won't care if it's centralized\n- being centralized allows Grace to handle arbitrarily large files, and to give users controls for which files get retrieved locally\n- being centralized allows Grace to scale up well by relying on mature Platform-as-a-Service components\n- it's 2024, and writing software that requires a file server seems... dated\n- I'm not sure I'm smart enough to write a better DVCS protocol and storage layer than Git\n- the \"I have to be able to work disconnected\" scenario is less-and-less important\n  – a growing number of developers today use cloud systems as part of their development and production environments, and if they're not connected to the internet, having their source control unavailable is the least of their problems\n  – in the coming years, satellite Internet will provide always-on, high-speed connections in parts of the world that were previously cut-off or limited\n\nAnd, by the way,\n\n### We're all using Git as a centralized VCS anyway\n\nAlmost _everyone_ uses Git in a pseudo-centralized, hub-and-spoke model, where the \"real\" version of the repo – the hub – is centralized at GitHub / GitLab / Atlassian / some other server, and the spokes are all of the users of the repo. In other words, we're already using Git as a centralized version control system, we're just kind-of pretending that we're not, and we're making things more complicated for ourselves because of it.\n\n### Centralized isn't necessarily less antifragile\n\nI get it. In an absolutely worst-case scenario, where `<insert world calamity here>` happens, isn't it better to have multiple full copies of a repo distributed across all of the users?\n\nHere's how I think about this for Grace:\n\n- In a professionally-run instance of Grace, the infrastructure generally will be PaaS services from serious cloud providers. They'll use georeplication of data to enable disaster recovery, and failover drills to make sure it all works (and lots of other professional things) and your repo will survive regional disasters.\n- Users of a repo will have the most recent versions of the branches that they're working on, and some number of previous versions, in Grace's local object cache. While that's not the entire history of the repo, if you're totally offline, it's enough to build the current version and keep going.\n- Grace is event-sourced, and if you really wanted to hook into every `New file version created` event and make your own backups, you'll be able to.\n- Grace will have an \"export to Git\" feature. If you want to (periodically) export the current state of your repo to a Git-format file, you'll be able to.\n\n#### What if my repo gets \"banned\" or \"shut down\" or something like that?\n\nThere are many good reasons, and some not-so-good reasons I could imagine, that a repo might be shut down by a provider. GitHub and GitLab and Atlassian and Azure DevOps and every hoster everywhere all have to deal with those decisions regularly.\n\nWithout getting into a discussion of which reasons fall into the good vs. not-so-good categories, I'll just say, again, you'll have the latest version of the branches that you're working on downloaded locally – in other words, the ones that matter. That's enough to keep going or start over if it comes down to it.\n\n### Every other service we use is centralized, this just seems weird because we're used to Git\n\nMy email is centralized at Microsoft and Google, depending on the account. I don't have a full local copy of a mathematically-validated graph of all of my banking transactions to do my online banking. I have tons of files in OneDrive, but they're not all downloaded to my SSD. Etc.\n\nHaving centralized source control just seems weird because we're not used to it anymore. Having a full local copy of all of the history of a repo seems like a warm, cozy, safe thing, but, really, how often to you _actually_ need the entire history of the repo to be local? How often do you _actually_ look at it locally vs. looking at history on GitHub / GitLab / etc. online?\n\nGrace can provide the views you need, they'll just be run on the server.\n\nIf you really need a full local copy of your repo with all of its history, Git's still your uncle. Most of us can let that go.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Performance; or, Isn't centralized version control slower?\n\nI've been around long enough to have used a couple of the older CVCS's, and I understand the reputation of them as feeling, just... slower. And heavier. That's not what Grace is like.\n\nGrace is designed to feel lightweight, and to be _consistently fast_, by which I mean:\n\n1. running a command in Grace should never take so long that it takes you out of your flow\n2. running the same Grace command (i.e. `grace commit` or `grace diff` or whatever) in the same repo should take roughly the same amount of time _every time_, within a few hundred milliseconds (i.e. within most users' tolerance for what they would call \"consistent\").\n\n### Git is sometimes faster than Grace...\n\nGit is really fast locally, and because almost every command in Grace will require at least one round-trip to the server, there are some commands for which Grace will never be as fast as Git. In those situations, my aim is for Grace to be as-fast-as-possible, and always _fast enough_ to feel responsive to the user. I expect most Grace commands to execute in under 1.0s on the server, so... slightly slower than local Git, but _fast enough_ to be good UX.\n\n### ...except when Grace is faster than Git\n\nThere are also scenarios where Grace will be faster than Git – usually scenarios where Git communicates over a network – because, in Grace, the \"heavy lifting\" of tracking changes and uploading new versions and downloading new versions will have been done already, in the background (with `grace watch`). In those use cases, like `grace checkpoint` and `grace commit`, the command is just creating a new database record, and that's faster than `git push`.\n\nSo, Grace is designed to be _fast_, i.e. fast enough to keep users in flow, and to be _consistent_, i.e. users quickly develop muscle-memory for how long things take, helping them stay in flow.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## How much Git should we keep?\n\nVarious version control projects over the years, and today, have attempted to be completely new front-ends for Git, while keeping Git's back-end storage format. Or maybe they keep the Git network protocol, but have a different client, or a different storage format. Or something like that. They keep some of Git but not all of it, and hope to deliver a better UX that Git.\n\nMy observation is: no matter how confusing Git itself is, none of those approaches have ever taken any market share away from Git.\n\n### Even Git can't be the new Git\n\nGit itself has tried to modernize a little bit over the years. For example, in 2020, Git added the `git switch` and `git restore` commands. In my completely informal and anecdotal asking around, no one has ever heard of them. 2½ years later, they're still marked as \"EXPERIMENTAL\".\n\nI point this out because I believe that, in most people's minds, the command surface of Git is locked. Once they go through the pain of learning enough Git to get by, very few people want to continue going deeper or to re-learn new ways of using it every few years.\n\nWhen searching for help about Git, the search results overwhelmingly reflect older ways of using Git, and it will take years before those search results reflect newer ways of using it, where \"newer\" = \"the last 4-5 years\". Even new web content about Git sometimes uses old constructs.\n\nIt's exactly in those years that I believe a new version control system will start taking market share away from Git and become the Cool New Thing.\n\n### So how much?\n\nSo... how much Git should we keep when we create new version control systems?\n\nMany projects seem to think: _a lot_, because (the thinking goes) in order to get adoption, you have to be Git-compatible.\n\nGrace's answer is: _not very much_. It definitely borrows things from Git, but, fundamentally, Grace is _really_ different.\n\nGrace says: It's time to start with a blank sheet of paper.\n\nOnly time will tell if this design decision is right – i.e. if it wins hearts and minds – but given that the hang-onto-Git-somewhat-but-do-it-differently path is already being explored by other projects, I want to see what can happen when we really let go of Git.\n\nI mean, someone's gotta do it.\n\n### Import and export, but not sync\n\nGrace will support one-time import from Git, and snapshot-style export to a `git bundle` file, but supporting two-way synchronization between Grace and Git is an explicit non-goal. Getting through the edge cases of that would take a while, and I have much higher-priority things to do.\n\nGrace's design is so different from Git's that spending time trying to make them fit together is less about composition, and more about duct-taping two totally different things together.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Scalability\n\nGrace has no binary data format of its own, and relies on object storage (i.e. Azure Blob Storage, AWS S3, etc.) to store individual versions of files. Likewise, Grace's metadata is stored as documents in a database (i.e. Azure Cosmos DB, MongoDB, Redis, etc.) and not in a file on a filesystem. Therefore, to a large extent, the scalability of an installation of Grace depends on the Platform-as-a-Service components that it is deployed on.\n\nBecause Grace uses the Actor pattern extensively, Grace benefits when more memory is available for each server container instance, as Grace will automatically use that memory as a cache, reducing pressure on the underlying database. And because Grace Server is stateless, and Dapr's Actor Placement service automatically rebalances whenever an application instance is added or removed, Grace can scale up and scale down automatically as traffic increases or decreases, using standard [KEDA](https://keda.sh/) counters to drive those actions.\n\nI haven't yet run large-scale load tests, but... if the database used for Grace can support thousands of transactions/second, and the object storage service can handle thousands of transactions/second (and the message bus and the observability system etc.), then between that and Grace's natural use of memory for caching, Grace Server *should* be able to scale up and scale out pretty well. The smaller-scale load tests I've run so far have been promising.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Monorepos\n\nDefining what a \"large repository\" or a \"monorepo\" is isn't straightforward. \"Large\" can mean different things:\n\n- a large number of files\n- a large number of versions of files\n- a large number of updates-per-second\n- a large number of users/services hitting the repo at the same time\n- large binary files versioned in the repo\n- some or all of the above, all at the same time.\n\nGrace is designed to handle all of these scenarios well. Grace decomposes a repository from being \"one big ball of bytes\" into being individual files in object storage, and individual documents in a database representing the state of repositories, branches, directories, and everything else. This way of organizing the data about the repository allows commands and queries to run just as fast for monorepos as they do for small and medium-sized repos.\n\nThere are, of course, some operations that will take longer on larger repositories (`grace init` is an obvious example where a lot of files might need to be downloaded), but, in general, Grace Server's performance shouldn't degrade as the repository size grows. (Grace CLI as well... *if* you're running `grace watch`).\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n[^StefanLesser]: From [How to adopt Christopher Alexander’s ideas in the software industry](https://stefan-lesser.com/2020/10/27/how-to-adopt-christopher-alexanders-ideas-in-the-software-industry/).\n\n[^output]: That's the intention, anyway. I have some work to do on some of the commands to light all of that up.\n"
  },
  {
    "path": "docs/Design concepts/Backups.md",
    "content": "# Backups in Grace\n\nLike any computer system, Grace needs to have backups, and to test restoration from those backups.\n\nBecause Grace's data is stored in both a database (like Cosmos DB, Cassandra, etc.) and in an object storage system (like Azure Blob Storage, or S3), we need to have a way to define what a \"snapshot\" is across those systems, and we also probably need to understand that it's possible that the last few seconds before some failure occurs may involve losing some data. Over time, we'll chaos-test Grace and see if we can minimize the amount of data loss.\n\nThe thing is... Grace can run on many different platforms, and each platform has its own backup and restore mechanisms. Every Grace service provider, and everyone self-hosting Grace, will have to decide how to do backups and restores.\n\nWith that said, there are some general perspectives that need to be kept in mind when deciding what it means to have a backup of Grace.\n\n## Geo-redundancy\n\nThe first, obvious form of \"backup\" is to use geo-redundant features of the underlying storage systems. Because I'm an Azure guy, I'll use Azure examples; I know similar features exist in AWS / GCP / etc.\n\nAzure Cosmos DB can synchronize data between many regions around the world within seconds, just through configuration. Azure Blob Storage has a geo-redundant option that pairs your main region with a backup region at least several hundred kilometers away. It's basically malpractice to not use these features when they're available with a click.\n\n## Database backups\n\nWhatever form of backup for the actor storage database you choose, it should be possible to restore the database to a point in time.\n\nIf you have the exact time of the backup, you can use that to filter any changes to object storage after that time in the event of failure and restoration.\n\nOver time, we expect to create, or to have Grace service providers create and share, mature utilities to assist in handling all of this.\n\n## User backups\n\nAlthough many users will be comfortable with the idea that their Grace service provider is adequately backing up their repositories, some will want to have their own backups. There is, of course, a comfort in having every Git repository instance be a full copy. This is especially true for users who are self-hosting Grace. They may want to have a backup of their repositories in a different region, or even in a different cloud provider.\n\n### Git bundle file\n\nGrace will support exporting a repository to Git bundle format. Because the creation and maintenance of those files can be compute-intensive, we'd prefer to have this done infrequently... maybe only on promotion. We'll have to have a way to do this on-demand, of course.\n\n### Object storage backups\n\nUsers will want copies of the files in object storage. Those copies can be done either by the Grace service provider, or by the user.\n\nIf a user wants to subscribe to Grace's event stream, they can use that to keep their own object storage in sync with the Grace service provider's object storage, all the way down to saves. Whenever an event happens that they feel they want to make sure they have a backup for - at least every promotion - they can copy the correct versions of files from object storage to their own object storage. Grace service providers will have to decide how to handle this, how to charge for network egress, etc.\n\nAlternatively, a Grace service provider may offer a service themselves where a user can supply their own object storage credentials, and the service provider will copy the files to the user's object storage. Again, there are concerns about how to charge for the service, and how to charge for network egress.\n"
  },
  {
    "path": "docs/Design concepts/Directory and file-level ACL's.md",
    "content": "# Directory and File-level ACL's\n\n"
  },
  {
    "path": "docs/Frequently asked questions.md",
    "content": "# Frequently Asked Questions\n\n(_or, what I imagine they might be_)\n\nFor deeper answers to some of these, please read [Design and Motivations](Design%20and%20Motivations.md).\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Is Grace centralized or decentralized?\n\nGrace is a centralized version control system (VCS).\n\n## I thought centralized version control systems were really slow.\n\nWell, that's not a question... but... yeah, in some ways, the older centralized VCS's could feel slower, and heavier, especially when dealing with branches.\n\nGrace is not like that. Grace is new, modern, lightweight, and very fast.\n\n## Why did you create Grace?\n\nIn the 2022 StackOverflow Developer Survey – the most recent one where they tracked version control usage – Git was at 93.87% adoption. Git has won, no doubt.\n\nAnd there's sort-of nowhere for it to go but down.\n\nI've been around long enough to see different technologies rise and fall. Some have shorter market cycles (web UI frameworks, for instance), and some have longer market cycles, like hierarchical -> relational -> No-SQL databases, or popular social media apps. I've seen technologies that had almost 95% market share, with very long cycles, like Windows and Windows Server, eventually lose market share for one reason or another.\n\nGit is 19 years old now. It doesn't have the easiest UX, to say the least. Many projects are exploring version control right now to see where it might go next. Git won't stay near 95% forever. Nothing ever does.\n\nThe thing that's probably going to take Git down is monorepos. Although I think large monorepos are a terrible idea – and I strongly recommend that you use multiple code repositories and proper versioning with a package or artifact repository – the trend right now is toward monorepos. Git doesn't do monorepos well, or, to be more precise, Git only does monorepos well by breaking the original contract of Git as a distributed VCS by using partial clones, or filtered partial clones, and therefore treating Git as a centralized VCS.\n\nOf course, if you're using GitHub or GitLab or Azure DevOps, you're already doing centralized version control, you're just doing it with a decentralized VCS with bad UX, which doesn't make a lot of sense when you think about it.\n\nGrace is my offering to that search for what's next. Grace's design is my attempt to bring ease-of-use into a corner of our world that hasn't had much of that lately, and to connect us together in a different way than ever before.\n\n## I like the way Git does branches.\n\nAgain with the not-question... I do too. I think that the lightweight branching in Git is one of the major reasons that it won.\n\nThat's why I kept lightweight branching in Grace. Create and delete branches to your heart's content.\n\n## What about when I'm disconnected?\n\nWell, you won't be able to run many Grace commands. And you probably won't be able to do lots of other things that you usually do.\n\nMore and more of us rely on cloud services and connectivity to the Internet just to do our jobs. Think about this: if your Internet connection went down, could you continue to do your job as a developer, or would you have to stop? Some of you could keep working, but if you can't, not having a connection to your source control server is the least of your concerns compared to not having a connection to Azure or AWS or wherever your cloud stuff is... not to mention Copilot and StackOverflow and your favorite search engine.\n\nWith the growth of satellite Internet, we're connecting more and more of the world at high-enough bandwidth to use centralized version control without issue. And I'm not designing for the 0.000001% \"but I'm on a flight without Internet\" scenario.\n\nIf being able to use local version control while you're not connected to the Internet is an important scenario for you, please use Git. It's great at that. I'm guessing that there's still a small – important, but small – percentage of programmers in the world that _really_ need that. For the rest of us, the vast majority of us, assuming a working Internet connection isn't a concern in 2024, and will be even less of a concern in 2026, 2028, etc.\n\nAnyway, Grace won't stop you from continuing to edit the local copies of your files that you already have. When your Internet connection resumes, `grace watch` will catch you up immediately.\n\n## Have you thought about using blockchain to...\n\nNo. 🤦🏼‍♂️ Just... no.\n\n## Can Grace do live two-way synchronization with Git repositories?\n\nNo, it can't. Two-way synchronization is a non-goal.\n\nOne-time initial import from a Git repo will be supported, and point-in-time export to a `git bundle` file will be supported, but not continuous two-way synchronization.\n\nGrace just has a fundamentally different design than Git. That's intentional. Two-way synchronization would involve a messy translation between what Git calls a _merge_ and what Grace calls a _promotion_, and I don't see a good way right now to handle that well without writing a _lot_ of code and handling a _lot_ of edge cases, and that's time better spent on everything else that still needs doing.\n\nAlso, I don't think that new version control systems need to sync with Git to catch on. Git didn't have two-way sync with any of the VCS's that we all migrated from, and it didn't stop us from changing over. We did the migrations over some weekend, and Monday morning we were using Git, and we got on with our lives.\n\n## What are the scalability limits for Grace?\n\n### Hopeful answer\n\nIt depends on the PaaS services that Grace is deployed on. In general, Grace itself is very fast, and will take advantage of the speed and scale of the underlying cloud services it depends on.\n\nI know Microsoft Azure well, so when I think about running Grace on services like Azure Kubernetes Service, Azure Cosmos DB, Azure Blob Storage, Azure Event Hubs, Azure Service Bus, Azure Monitor, and others, I look at Grace Server as orchestrating the usage of insanely high-scale PaaS products, and that's exactly what it's designed to do.\n\nThe stateless nature of Grace Server, and the use of the Actor Pattern, should allow for a significant number of concurrent users without too much hassle. In particular, Grace is designed so that data that the server needs when you run common CLI commands will already be in-memory most of the time. If it's not, that data will usually be under 10ms away in a document database.\n\nThe only load testing that I've done saturated my personal [Azure Cosmos DB Request Units](https://learn.microsoft.com/en-us/azure/cosmos-db/request-units), but didn't stress Grace Server at all, which is what I expected. I haven't tested higher than 10,000 RU's, but I expect that when I do, I'll find some things to improve, and then Grace should be able to handle thousands of transactions/second.\n\n### Actual current answer\n\nI haven't done any truly high-scale load testing yet. I'm not sure.\n\nI _can_ tell you that I've tested repositories of up to 100,000 files and 15,000 directories, with Grace deployed using Azure CosmosDB and Azure Blob Storage. If `grace watch` is running, client performance for most commands on those large repositories is around 0.8-1.0s (which includes 1 or 2 80ms roundtrips to the Azure data center). Performance on small- and medium-sized repositories is around 0.6-0.8s. Grace Server performance is unaffected by repository size for most commands. These times are from debug builds.\n\nI've also tested individual file sizes up to 10GB. I'm not sure that 10GB files should fall under the purview of version control–they should probably be versioned blobs in an object storage service – but we'll see what happens. Grace doesn't have a technical limitation on file size (it's a uint64).\n\nEach command, on its own, runs quickly enough to make me happy. I hope they all still do at scale.\n\n## What does Grace borrow from Git?\n\nA lot of things.\n\nGrace keeps the ephemeral working directory, and the idea of a `.grace/objects` directory for each repo.\n\nGrace keeps the lightweight branching, so you can continue to create and discard branches just as quickly as we do in Git.\n\nGrace borrows the use of SHA-256 values as the hash function to uniquely identify files and directories.\n\nWe even borrowed the algorithm to decide if a file is text or binary.[^binary]\n\nAnd much more. Grace owes a debt of gratitude to Git and to its maintainers.\n\n## What about the other new version control projects?\n\nThere are a few really good VCS projects going on right now. It's exciting to see.\n\nI'm fortunate enough to know the folks involved in [Jujutsu](https://github.com/martinvonz/jj). They're doing incredible work. I'm aware of [Pijul](https://pijul.org/). I see Meta has just announced [Sapling](https://sapling-scm.com/). [PlasticSCM](https://www.plasticscm.com/) is really good; I have enormous respect for Pablo and his whole team. I know there are others. If you're interested, I recommend searching YouTube for some good, short introductions to all of them.\n\nAll I can say is: Grace has its own design philosophy, and its own perspective on what it should feel like to be using version control, and that's what I'm passionate about. I think of it as a friendly competition to see which one wins with the next generation of developers.\n\nOne of them will catch on and get popular soon enough.\n\n## On _HN_, FOSSBro761 says: _\"Grace \\<something something\\> M$ \\<something\\>...\"_\n\nSigh. Um, OK. So...\n\nIf you look at my GitHub or LinkedIn profile you'll see that I work for GitHub, which means that I work for Microsoft. With that said, Grace is a personal side-project. It's probably the case that I wouldn't have thought to start a new version control system without having been at GitHub, but now I've caught the version control bug. (Some people tried to warn me that that happens, but I didn't listen.)\n\nIt's a fascinating area to work in, and one that will see exciting innovation in the coming years. Grace's existence does not imply anything at all about GitHub's continued, massive, ongoing ❤️ and support of Git, or about GitHub's future direction in source control, or about anything else about GitHub.\n\nAll opinions and work here is personal, and not endorsed by my employers. (There, I said it.)\n\n_What actually happened was..._\n\nI started thinking about Grace in December 2020, and it became my personal 2021 pandemic lockdown side-project. I invented it. No one told me to invent it, I just did.\n\nI chose .NET because it's what I know and trust, and because trying to write my first, big functional system was challenging enough without also having to learn a new ecosystem at the same time. I chose F# because I wanted to think functionally as I explored.\n\nI chose to do it at all because I realized that _something_ was eventually going to replace Git, and I had some opinions about what that should be, and about what direction we should take as an industry in UX for source control. The only way to communicate that effectively was to start writing code and see if I could build something worthwhile.\n\nThat's the origin story. Just a guy who had an idea he couldn't let go of, using good tools that he knows well, and a few tools that he's learning.\n\n\n## How can I get involved?\n\nWhy, thank you for asking. ❤️\n\nEverything helps. Feel free to file an Issue in the repo. Please join us over in Discussions.\n\nI'm working right now to get Grace in better shape for debugging for everyone. I confess the debug workflow is very much tailored to me and my local machine at the moment. I'm going to fix that, and when I do, I'll post instructions for how to write code for and debug Grace as easily as possible.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n[^binary]: The algorithm is: is there a 0x00 byte anywhere in the first 8,000 bytes? If there is, it's assumed to be a binary file. Otherwise, it's assumed to be a text file.\n"
  },
  {
    "path": "docs/How Grace computes the SHA-256 value.md",
    "content": "# Computing the SHA-256 value for files and directories\n\n## Introduction\n\nGrace uses the [SHA-256](https://en.wikipedia.org/wiki/SHA-2) algorithm to compute cryptographically verifiable hash values for files and directories stored in Grace. The use of SHA-256 hashes for this purpose is inspired by Git's use of SHA-1, and their ongoing work to migrate to SHA-256.\n\nThe SHA-256 hash values are used throughout Grace to identify unique versions of files and directories, primarily to minimize the size of change captured by each reference (i.e. each save, checkpoint, commit, and tag).\n\nHash values in Grace are meant to provide cryptographic proof that the directory and file versions stored for each reference match what was originally uploaded. To be more precise, when a user downloads a specific version of a branch, the files and directory versions should provably be the same versions that were originally stored in Grace. Grace Server, as part of maintenance routines, should also be able to verify file and directory versions at any time.\n\nUnlike Git, the SHA-256 values provide no linkage between references. Each SHA-256 value is specific to that version of the repository, with no connection to previous or subsequent versions. This enables Grace to be able to delete references and versions, such as saves that are no longer necessary, without the manipulation of history required in Git.\n\nThe choice of SHA-256, as opposed to SHA-384 or other stronger algorithms, comes from a desire to provide excellent runtime performance with strong cryptographic hashing. SHA-256 has been studied extensively [for over 20 years](https://en.wikipedia.org/wiki/SHA-2), and [seems to be collision-resistant to quantum algorithms](https://crypto.stackexchange.com/questions/59375/are-hash-functions-strong-against-quantum-cryptanalysis-and-or-independent-enoug). Because Grace, like Git, uses it here solely for hashing, and not for encryption, any potential long-term issues with SHA-256 leave only a small opportunity for misuse.\n\n> This information is valid as of April, 2022. The implementation may change as Grace matures.\n\n## Implementation\n\nIn ordinary usage, SHA-256 values are computed by Grace CLI, and those values are used when uploading versions of files and directories, and when creating references on the server.\n\nSpecifically, Grace relies on the .NET implementation of [SHA-256](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.sha256), found in the [System.Security.Cryptography](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography) namespace.\n\n.NET implementations of Grace clients are welcome to use the hashing implementation in Grace.Shared, which is used by both Grace CLI and Grace Server. Implementations in other languages will need to implement this algorithm separately.\n\nGrace Server rechecks each SHA-256 hash as versions arrive at the server. In the event of a discrepancy, which could indicate malicious behavior, the invalid versions will be deleted, and Grace administrators and repository owners will be notified.\n\n### Files\nWhen computing the SHA-256 value for a file, Grace uses a stream - FileStream when reading a local file, and Stream when reading a file from object storage - and the [IncrementalHash](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.incrementalhash) class, to keep memory use constant, no matter the size of the file.\n\nThe SHA-256 value for a file is computed with the following algorithm.\n\n1. An instance of the [IncrementalHash](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.incrementalhash) class is created using the SHA-256 hash algorithm.\n2. Bytes from the file are read from the stream into a 64KB buffer, and appended to the IncrementalHash's input data, until the stream is consumed.\n3. The relative path of the file (based on the repository root), represented as a string, is converted to bytes using UTF-8 encoding, and appended to the IncrementalHash's input data.\n\n    > For consistency between Windows and Unix-y OS's, Grace converts backslashes `\\` in the relative path into forward slashes `/` before converting to bytes.\n\n    - For example: a file is being uploaded on Windows with full path name `C:\\Source\\MyRepo\\SomeDir\\myfile.md`. The root of the repository is in `C:\\Source\\MyRepo`. Therefore, the relative path of the file is `.\\SomeDir\\myfile.md`. This string will be converted to `./SomeDir/myfile.md` before being converted to a byte array and appended to the IncrementalHash's input.\n4. The length of the file, represented as an `Int64` value, is converted to bytes and appended to the IncrementalHash's input data.\n5. The SHA-256 hash is computed as a `byte[]`.\n6. The SHA-256 hash is converted to a string by converting each byte to a two-character hexadecimal value.\n    - For example, `byte[]{0x43, 0x2a, 0x01, 0xfa}` would be converted to a string `\"432a01fa\"`.\n\nThe code for this can be found in the Grace.Shared project, in Utilities.Shared.fs.\n\n``` fsharp\nlet computeSha256ForFile (stream: Stream) (relativeFilePath: String) =\n    task {\n        let bufferLength = 64 * 1024\n        let buffer = ArrayPool<byte>.Shared.Rent(bufferLength)\n\n        try\n            // 1. Create an IncrementalHash instance.\n            use hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256)\n            // 2. Read bytes from the stream and feed them into the hasher.\n            let mutable loop = true\n            while loop do\n                let! bytesRead = stream.ReadAsync(buffer.AsMemory(0, bufferLength))\n                if bytesRead > 0 then\n                    hasher.AppendData(buffer.AsSpan(0, bytesRead))\n                else\n                    loop <- false\n            // 3. Convert the relative path of the file to a byte array, and add it to the hasher.\n            hasher.AppendData(Encoding.UTF8.GetBytes(relativeFilePath))\n            // 4. Convert the Int64 file length into a byte array, and add it to the hasher.\n            hasher.AppendData(BitConverter.GetBytes(stream.Length))\n            // 5. Get the SHA-256 hash as a `byte[]`.\n            let sha256Bytes = hasher.GetHashAndReset()\n            // 6. Convert the SHA-256 value from a byte[] to a string, and return it.\n            //    Example: byte[]{0x43, 0x2a, 0x01, 0xfa} -> \"432a01fa\"\n            return byteArrayAsString(sha256Bytes)\n        finally\n            ArrayPool<byte>.Shared.Return(buffer, clearArray = true)\n    }\n```\n\n### Directories\nThe SHA-256 hash of a directory is computed using the following algorithm.\n\n1. The relative path of the directory (based on the repository root), represented as a string, is converted to bytes and stored in a `List<byte>`.\n    - For example: a directory version is being uploaded with full path name `C:\\Source\\MyRepo\\SomeDir\\SomeSubDir`. The root of the repository is in `C:\\Source\\MyRepo`. Therefore, the relative path of the directory is `.\\SomeDir\\SomeSubDir`. This string will be converted to `./SomeDir/SomeSubDir` before being converted to a byte array and used to create the `List<byte>`.\n2. The list of _subdirectories_ is sorted by name, using [CultureInfo.InvariantCulture](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.invariantculture). The SHA-256 value for each subdirectory is converted to a `byte[]` using UTF-8 encoding, and appended, in order, to the `List<byte>`. The sorted list of subdirectories does not include the `/.` and `/..` directories.\n3. The list of _files_ in the directory is sorted by name, using [CultureInfo.InvariantCulture](https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.invariantculture). The SHA-256 value for each file is converted to a `byte[]` using UTF-8 encoding, and appended, in order, to the `List<byte>`.\n4. The entire `List<byte>` is used as input to compute the SHA-256 value. The SHA-256 value is represented as a `byte[]`.\n5. The SHA-256 hash is converted to a string by converting each byte to a two-character hexadecimal value.\n    - For example, `byte[]{0x43, 0x2a, 0x01, 0xfa}` would be converted to a string `\"432a01fa\"`.\n\n### Validation\nGrace will provide a command - `grace branch verify` - that will compute the SHA-256 values for on-disk versions of files and directories, and compare them to the SHA-256 values stored in Grace's database.\n"
  },
  {
    "path": "docs/Mermaid diagrams.md",
    "content": "# Mermaid diagrams\r\n\r\n## Starting state\r\n\r\n```mermaid\r\n%%{init: { 'logLevel': 'debug', 'theme': 'default', 'gitGraph': {'showBranches': true, 'showCommitLabel': false}} }%%\r\n    gitGraph\r\n        commit tag: \"ce38fa92\"\r\n        branch Scott\r\n        branch Mia\r\n        branch Lorenzo\r\n        checkout Scott\r\n        commit tag: \"87923da8: based on ce38fa92\"\r\n        checkout Mia\r\n        commit tag: \"7d29abac: based on ce38fa92\"\r\n        checkout Lorenzo\r\n        commit tag: \"28a5c67b: based on ce38fa92\"\r\n        checkout main\r\n```\r\n\r\n## A promotion on `main`\r\n\r\n```mermaid\r\n%%{init: { 'logLevel': 'debug', 'theme': 'default', 'gitGraph': {'showBranches': true, 'showCommitLabel': false}} }%%\r\n    gitGraph\r\n        commit tag: \"ce38fa92\"\r\n        branch Scott\r\n        branch Mia\r\n        branch Lorenzo\r\n        checkout Scott\r\n        commit tag: \"87923da8: based on ce38fa92\"\r\n        checkout Mia\r\n        commit tag: \"7d29abac: based on ce38fa92\"\r\n        checkout Lorenzo\r\n        commit tag: \"28a5c67b: based on ce38fa92\"\r\n        checkout main\r\n        commit tag: \"87923da8\"\r\n```\r\n\r\n## Branching model\r\n\r\n```mermaid\r\ngraph TD;\r\n    A[master] -->|Merge| B[release];\r\n    B -->|Merge| C[develop];\r\n    C -->|Merge| D[feature branch];\r\n    D -->|Feature Completed| C;\r\n    B -->|Release Completed| A;\r\n    E[hotfix branch] -->|Fix Applied| A;\r\n    E -->|Fix Merged into| C;\r\n\r\n    classDef branch fill:#37f,stroke:#666,stroke-width:3px;\r\n    class A,B,C,D,E branch;\r\n```\r\n\r\n## Entities\r\n\r\n```mermaid\r\nflowchart LR\r\n  %% Arrows point from \"FK holder\" --> \"referenced DTO\".\r\n  %% Labels are the FK field(s). [*] means list/collection. (optional) means option.\r\n\r\n  subgraph Identity\r\n    OwnerDto[\"OwnerDto<br/>PK: OwnerId\"]\r\n    OrganizationDto[\"OrganizationDto<br/>PK: OrganizationId\"]\r\n    RepositoryDto[\"RepositoryDto<br/>PK: RepositoryId\"]\r\n  end\r\n\r\n  subgraph VersionGraph\r\n    BranchDto[\"BranchDto<br/>PK: BranchId\"]\r\n    ReferenceDto[\"ReferenceDto<br/>PK: ReferenceId\"]\r\n    DirectoryVersionDto[\"DirectoryVersionDto<br/>PK: DirectoryVersionId\"]\r\n    DiffDto[\"DiffDto<br/>Diff between DirectoryVersionIds\"]\r\n  end\r\n\r\n  subgraph PolicyReview\r\n    PolicySnapshot[\"PolicySnapshot<br/>PK: PolicySnapshotId\"]\r\n    Stage0Analysis[\"Stage0Analysis<br/>PK: Stage0AnalysisId\"]\r\n    ReviewPacket[\"ReviewPacket<br/>PK: ReviewPacketId\"]\r\n    ReviewCheckpoint[\"ReviewCheckpoint<br/>PK: ReviewCheckpointId\"]\r\n  end\r\n\r\n  subgraph PromotionSystem\r\n    PromotionGroupDto[\"PromotionGroupDto<br/>PK: PromotionGroupId\"]\r\n    IntegrationCandidate[\"IntegrationCandidate<br/>PK: CandidateId\"]\r\n    PromotionQueue[\"PromotionQueue<br/>Key: TargetBranchId\"]\r\n    GateAttestation[\"GateAttestation<br/>PK: GateAttestationId\"]\r\n    ConflictReceipt[\"ConflictReceipt<br/>PK: ConflictReceiptId\"]\r\n  end\r\n\r\n  subgraph WorkManagement\r\n    WorkItemDto[\"WorkItemDto<br/>PK: WorkItemId\"]\r\n  end\r\n\r\n  subgraph Reminders\r\n    ReminderDto[\"ReminderDto<br/>PK: ReminderId\"]\r\n  end\r\n\r\n  %% Identity hierarchy / tenancy\r\n  OrganizationDto -->|OwnerId| OwnerDto\r\n  RepositoryDto -->|OwnerId| OwnerDto\r\n  RepositoryDto -->|OrganizationId| OrganizationDto\r\n\r\n  %% Branch\r\n  BranchDto -->|OwnerId| OwnerDto\r\n  BranchDto -->|OrganizationId| OrganizationDto\r\n  BranchDto -->|RepositoryId| RepositoryDto\r\n  BranchDto -->|ParentBranchId| BranchDto\r\n  BranchDto -->|BasedOn| ReferenceDto\r\n  BranchDto -->|LatestReference / LatestPromotion / LatestCommit / LatestCheckpoint / LatestSave| ReferenceDto\r\n\r\n  %% Reference\r\n  ReferenceDto -->|OwnerId| OwnerDto\r\n  ReferenceDto -->|OrganizationId| OrganizationDto\r\n  ReferenceDto -->|RepositoryId| RepositoryDto\r\n  ReferenceDto -->|BranchId| BranchDto\r\n  ReferenceDto -->|DirectoryId| DirectoryVersionDto\r\n  ReferenceDto -->|Links.BasedOn| ReferenceDto\r\n  ReferenceDto -->|Links.IncludedInPromotionGroup / Links.PromotionGroupTerminal| PromotionGroupDto\r\n\r\n  %% DirectoryVersion\r\n  DirectoryVersionDto -->|OwnerId| OwnerDto\r\n  DirectoryVersionDto -->|OrganizationId| OrganizationDto\r\n  DirectoryVersionDto -->|RepositoryId| RepositoryDto\r\n  DirectoryVersionDto -->|DirectoryVersion.Directories| DirectoryVersionDto\r\n\r\n  %% Diff\r\n  DiffDto -->|OwnerId| OwnerDto\r\n  DiffDto -->|OrganizationId| OrganizationDto\r\n  DiffDto -->|RepositoryId| RepositoryDto\r\n  DiffDto -->|DirectoryVersionId1| DirectoryVersionDto\r\n  DiffDto -->|DirectoryVersionId2| DirectoryVersionDto\r\n\r\n  %% PolicySnapshot\r\n  PolicySnapshot -->|OwnerId| OwnerDto\r\n  PolicySnapshot -->|OrganizationId| OrganizationDto\r\n  PolicySnapshot -->|RepositoryId| RepositoryDto\r\n  PolicySnapshot -->|TargetBranchId| BranchDto\r\n\r\n  %% PromotionGroup\r\n  PromotionGroupDto -->|OwnerId| OwnerDto\r\n  PromotionGroupDto -->|OrganizationId| OrganizationDto\r\n  PromotionGroupDto -->|RepositoryId| RepositoryDto\r\n  PromotionGroupDto -->|TargetBranchId| BranchDto\r\n  PromotionGroupDto -->|Promotions.PromotionId| ReferenceDto\r\n\r\n  %% PromotionQueue\r\n  PromotionQueue -->|TargetBranchId| BranchDto\r\n  PromotionQueue -->|PolicySnapshotId| PolicySnapshot\r\n  PromotionQueue -->|CandidateIds| IntegrationCandidate\r\n  PromotionQueue -->|RunningCandidateId| IntegrationCandidate\r\n\r\n  %% IntegrationCandidate\r\n  IntegrationCandidate -->|OwnerId| OwnerDto\r\n  IntegrationCandidate -->|OrganizationId| OrganizationDto\r\n  IntegrationCandidate -->|RepositoryId| RepositoryDto\r\n  IntegrationCandidate -->|WorkItemId| WorkItemDto\r\n  IntegrationCandidate -->|PromotionGroupId| PromotionGroupDto\r\n  IntegrationCandidate -->|TargetBranchId| BranchDto\r\n  IntegrationCandidate -->|PolicySnapshotId| PolicySnapshot\r\n  IntegrationCandidate -->|BaselineHeadReferenceId| ReferenceDto\r\n  IntegrationCandidate -->|ReviewPacketId| ReviewPacket\r\n  IntegrationCandidate -->|LastCheckpointId| ReviewCheckpoint\r\n  IntegrationCandidate -->|GateAttestationIds| GateAttestation\r\n  IntegrationCandidate -->|ConflictReceiptIds| ConflictReceipt\r\n\r\n  %% GateAttestation\r\n  GateAttestation -->|OwnerId| OwnerDto\r\n  GateAttestation -->|OrganizationId| OrganizationDto\r\n  GateAttestation -->|RepositoryId| RepositoryDto\r\n  GateAttestation -->|CandidateId| IntegrationCandidate\r\n  GateAttestation -->|PolicySnapshotId| PolicySnapshot\r\n  GateAttestation -->|BaselineHeadReferenceId| ReferenceDto\r\n\r\n  %% ConflictReceipt\r\n  ConflictReceipt -->|OwnerId| OwnerDto\r\n  ConflictReceipt -->|OrganizationId| OrganizationDto\r\n  ConflictReceipt -->|RepositoryId| RepositoryDto\r\n  ConflictReceipt -->|CandidateId| IntegrationCandidate\r\n\r\n  %% WorkItem\r\n  WorkItemDto -->|OwnerId| OwnerDto\r\n  WorkItemDto -->|OrganizationId| OrganizationDto\r\n  WorkItemDto -->|RepositoryId| RepositoryDto\r\n  WorkItemDto -->|BranchIds| BranchDto\r\n  WorkItemDto -->|ReferenceIds| ReferenceDto\r\n  WorkItemDto -->|PromotionGroupIds| PromotionGroupDto\r\n  WorkItemDto -->|CandidateIds| IntegrationCandidate\r\n  WorkItemDto -->|ReviewPacketIds| ReviewPacket\r\n  WorkItemDto -->|ReviewCheckpointIds| ReviewCheckpoint\r\n  WorkItemDto -->|GateAttestationIds| GateAttestation\r\n\r\n  %% ReviewPacket\r\n  ReviewPacket -->|OwnerId| OwnerDto\r\n  ReviewPacket -->|OrganizationId| OrganizationDto\r\n  ReviewPacket -->|RepositoryId| RepositoryDto\r\n  ReviewPacket -->|CandidateId| IntegrationCandidate\r\n  ReviewPacket -->|PromotionGroupId| PromotionGroupDto\r\n  ReviewPacket -->|PolicySnapshotId| PolicySnapshot\r\n  ReviewPacket -->|GateSummary.GateAttestationIds| GateAttestation\r\n\r\n  %% ReviewCheckpoint\r\n  ReviewCheckpoint -->|CandidateId| IntegrationCandidate\r\n  ReviewCheckpoint -->|PromotionGroupId| PromotionGroupDto\r\n  ReviewCheckpoint -->|ReviewedUpToReferenceId| ReferenceDto\r\n  ReviewCheckpoint -->|PolicySnapshotId| PolicySnapshot\r\n\r\n  %% Stage0Analysis\r\n  Stage0Analysis -->|OwnerId| OwnerDto\r\n  Stage0Analysis -->|OrganizationId| OrganizationDto\r\n  Stage0Analysis -->|RepositoryId| RepositoryDto\r\n  Stage0Analysis -->|ReferenceId| ReferenceDto\r\n  Stage0Analysis -->|WorkItemId| WorkItemDto\r\n  Stage0Analysis -->|CandidateId| IntegrationCandidate\r\n  Stage0Analysis -->|PolicySnapshotId| PolicySnapshot\r\n\r\n  %% Reminder\r\n  ReminderDto -->|OwnerId| OwnerDto\r\n  ReminderDto -->|OrganizationId| OrganizationDto\r\n  ReminderDto -->|RepositoryId| RepositoryDto\r\n\r\n```"
  },
  {
    "path": "docs/The potential for misusing Grace.md",
    "content": "# The potential for misusing Grace\n\nSomething I've thought about since the beginning of designing Grace is: how do we prevent ignorant and/or malicious management from using event data from Grace in the wrong way?\n\nSo, to start, and just to be clear:\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Using Grace events for monitoring is fucking stupid\n\nI'm aware of the potential to use things like \"how often did this person do a save / checkpoint / commit?\" as a metric, or as a proxy for productivity, or effort.\n\nAnd, because I've been a programmer for several decades, I, like you, am also aware of how fucking stupid and shortsighted it would be to do so.\n\nIt's not my intention for Grace to become a monitoring platform. And yet, there are features that Grace enables that simply can't happen without a modern, cloud-native event-driven architecture.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Git has timestamps, too\n\nSure, Git is distributed, and there's no way for the server to reach into your local repo and get status. If you use the feature-branch-gets-deleted method of using Git, then you're still pushing your feature branch and its commits up to the server before the merge-and-delete, and so you have that opportunity in Git to get some similar information that we're worried about in Grace. We haven't, as an industry, weaponized this data yet, so I'm hopeful we won't with any future version control system.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## So how do we prevent misuse?\n\nIf I turn off all of the events, it ruins a lot of the value proposition of Grace.\n\nBut there are approaches to it that I can imagine.\n\nOne idea is simply to not send events to the main Grace event stream for saves. The most obvious misuse is looking at saves, so maybe we just keep them as a thing for individual users, but we don't ship them over the service bus.\n\nAnother idea is just to turn off auto-saves for a repo entirely. Again, I hate losing this functionality, but it's a possibility.\n\nThere are other configurations of features I could imagine, but, ultimately, the design of it will have to come from the community, and I remain open to figuring out the right way to do it that enables the most flexibility with the most safety from idiot management.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## My promise: I'll be very vocal about it\n\nIn case it's not obvious from what I just said, I'm completely opposed to using events in Grace for any sort of monitoring of programmers. It's statistically stupid, it's harmful to even think there's any value in it.\n\nAnd I'll be vocal about it in every public presentation, in every talk with a potential user, and I'll get other leaders around Grace to say the same thing.\n\nI will do everything I can to make the point over and over and over, from every angle that I can.\n\nI know that's not an iron-clad guarantee that idiots won't idiot, but I'll sure as hell call them out for doing it, and demand that they stop.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n"
  },
  {
    "path": "docs/What grace watch does.md",
    "content": "# What `grace watch` does\n\n## Introduction\n\nOne of the most important pieces of Grace is `grace watch`. `grace watch` is a background process that watches your working directory for changes, automatically uploads changes after every save-on-disk, and enables you to run local tasks in response to events in the repository, including auto-rebase.\n\nMost Grace users will be programmers, and we're a more technical audience. We know that background processes can be used in ways that are helpful, harmful, or just wasteful. As someone asking you to run a background process, I have a special responsibility to be transparent with you about what `grace watch` does if you allow it to run. (Which you totally should.) I want you to have complete confidence that running `grace watch` is safe and trustworthy.\n\n## No dark patterns\n\nThere are certain behaviors that some products have around their use of background processes and schedulers that I find offensive. One example is Adobe Creative Cloud. I'm calling them out, not because I think they're deliberately evil, or worse than anyone else, but because it's an example that happens to be here in front of me, and to illustrate a *kind* of thing that Grace doesn't do.\n\nAdobe Creative Cloud really, really wants to make sure its [background processes](https://helpx.adobe.com/gr_en/x-productkb/global/adobe-background-processes.html) are always running. They use four different items to register them: one entry is added to the Windows Task Scheduler, one entry is added to start a process every time I log in, and two different services are configured to run as Windows Services that start before I even log in.\n\nIf all of that isn't annoying enough, those services are configured for `Automatic` start, not `Automatic (Delayed Start)`, which means that they start as soon as they can during boot, competing for system resources at the worst possible time, when they could easily wait a couple of minutes and run when there's less going on. Sigh.\n\nSo, if I don't want Adobe's background processes, I remove those four entries, and, hey, I'm good, right?\n\nNope. It seems like whenever I start an Adobe app, it recreates these entries, ensuring that if I'm going to use Adobe Creative Cloud, I have no choice but to put up with what they think should be running on my machine.\n\nWhat are these processes doing? What information are they collecting? How is that information being sent, and to whom? Are there third-party processors involved? Can I block it? Can I use a GDPR Data Subject Request to see or delete the information that's being sent?\n\nI have no idea, and the way they keep recreating these entries after I deliberately remove them doesn't build trust.\n\nUnless I write a PowerShell script to automate removing Adobe's registry entries and services, and ending those processes if they're running, and schedule it to run frequently, there's nothing I can do.\n\nSo... this kind of thing... stuff like that... you know what I mean? ... `grace watch` doesn't do that.\n\nHere's what it actually does.\n\n## Grace Watch - Compute, I/O, and network usage\n\nThis is meant to be an exhaustive list of the things that `grace watch` does. If it's not on this list, `grace watch` doesn't do it. If you believe I've missed something, please start a Discussion in the repo; if there's something to add, we'll create an issue and update this page.\n\nOf course, it's open-source, please feel free to examine [Watch.CLI.fs](https://github.com/ScottArbeit/Grace/blob/main/src/Grace.CLI/Command/Watch.CLI.fs).\n\n- `grace watch` uses a .NET [`FileSystemWatcher()`](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher?view=net-8.0) to watch your working directory for all changes and updates.\n- `grace watch` establishes a SignalR connection with Grace Server, and sends the BranchId of the parent branch. Grace Server then registers your connection in the correct notification groups.\n- When it starts, it scans the working directory and all (not-ignored) subdirectories and files for changes since the last time the local Grace Status file was updated.\n- When it starts, and at a couple of other times, it reads and deserializes the local Grace Status file. For small repos, it's well under 10K and is processed in about 1ms. The largest repositories I've tested had a ~53MB status file, and, if I recall correctly, reading and deserializing the data happened in low two-digit milliseconds on a four-year-old laptop.\n  - `grace watch` doesn't keep the Grace Status file in memory while it's running. It's so fast to read and deserialize it when it's needed that we make the tradeoff to release the memory rather than hold it indefinitely, especially given that there will be many times that the user isn't coding and `grace watch` should have as small of a memory footprint as possible.\n- When a new or updated file is detected, `grace watch` will:\n  - Copy the file to your user temp directory, using a system-generated temporary file name.\n  - Compute the SHA-256 hash of the file, using the algorithm described [here](How%20Grace%20computes%20the%20SHA-256%20value.md).\n  - Rename the file, inserting the SHA-256 hash, and move it to the repository's `.grace/objects` directory.\n  - Upload the file from `.grace/objects` to the Object Storage provider that the repository is configured to use.\n  - Recompute the directory versions from the file that was updated up to the root directory.\n  - Update the local Grace Status file with the new directory versions.\n  - Upload those recomputed directory versions to Grace Server.\n  - Create a Save reference by calling Grace Server's `/branch/createSave` endpoint.\n- When a promotion event from your parent branch is sent to `grace watch` by the server, `grace watch` will run auto-rebase.\n- Every 4.8 minutes, `grace watch` will recompute and rewrite the Grace interprocess-communication (IPC) file, which requires reading and deserializing the local Grace Status file. The size of the IPC file is under 1K for small repos, and scales with the number of directories in the repo. A repo with 275 directories would fit in a 10K IPC file, and a repo with 2,750 directories would fit in a 100K IPC file. They're usually very small.\n  > Long story about why we rewrite the file: Imagine that you're at the command line, and you run `grace checkpoint -m ...`. That instance of Grace uses the existence of the IPC file as proof that `grace watch` is running in a separate process. `grace watch` writes the IPC file as soon as it starts, and, deletes it in a `try...finally` clause when it exits. In other words: in any normal exit, including exits caused by unhandled exceptions, the IPC file will be deleted when `grace watch` exits. However: it's possible that `grace watch` could be killed before it has a chance to execute that `finally` clause. For instance, in Windows, if I open Task Manager, right-click on the `grace watch` process, and hit `End Task`, the process dies immediately, and does not execute the `finally` clause. To ensure that there's not a stale IPC file laying around, Grace checks the value of the UpdatedAt field; if it's more than 5 minutes old, Grace will ignore the IPC file and assume that `grace watch` isn't running. So: _that's_ why the IPC file gets refreshed every 4.8 minutes: it resets the UpdatedAt field so the file stays under 5 minutes old.\n- Once a minute, `grace watch` does the fullest of garbage collection:\n  \n  `GC.Collect(2, GCCollectionMode.Forced, blocking = true, compacting = true)`\n  \n  This ensures that `grace watch` keeps the smallest possible memory footprint, and takes single-digit µs when there's nothing to collect.\n  \n  > The .NET Runtime has excellent heuristics for when to run GC, but the biggest factor is memory pressure. If the OS isn't signaling that there's memory pressure on the system, GC's don't happen much. With `grace watch` running on a developer box with many GB's of RAM available, it's likely that there won't be much memory pressure, and it would rarely perform garbage collection. `grace watch` might look like it's taking up a lot of memory (from doing things like auto-upload, auto-rebase, and updating the IPC file), but it would all be Gen 0 references, ready to be collected.\n  >\n  > Given that there will be many times that a user isn't working in a repository, releasing memory proactively is the right thing to do.\n\n## Process Monitor: `grace watch` is very quiet\nThis is a Windows-specific story, but it illustrates what `grace watch` is doing, or _not_ doing, regardless of platform. Those of you familiar with Windows administration will be familiar with [Sysinternals Tools](https://learn.microsoft.com/en-us/sysinternals/), originally written by, and still partially maintained by, Microsoft Azure CTO Mark Russinovich. Before he was the CTO and Chief Architect of Azure, he was the Chief Architect of Windows, and he literally wrote the book _Windows Internals_, which is a great read if you're an OS nerd of any kind. Sysinternals Tools, after 20+ years, are still essential advanced tools to know on Windows.\n\nOne of the Sysinternals Tools is [Process Monitor](https://learn.microsoft.com/en-us/sysinternals/downloads/procmon), which allows you to watch every file, networking, registry, and process/thread event happening in the system in real-time. Process Monitor has excellent filtering, and when using it just to observe `grace watch`, what I can tell you is: if nothing from the list above is happening at exactly that moment, `grace watch` does nothing that Process Monitor can detect. No file events, no network events, just sometimes a thread creation/deletion event, managed by the .NET Runtime and not controlled by `grace watch`.\n\nI left it running for 20 minutes with Process Monitor; aside from the IPC file refreshes, `grace watch` showed no events at all.\n\nI can't make it any quieter than that.\n\n## Future items\n### Running local tasks\n`grace watch` is intended to be able to run local tasks in response to repository events, but, aside from `grace rebase`, that functionality hasn't been written yet. When it is, we'll document it and add it to the section above.\n\n### Telemetry\n`grace watch` does not currently collect or send any telemetry, but I intend to before Grace ships. There will be clearly-documented ways to turn it off, if you'd prefer, and proper GDPR (and related) handling in place. The intention of this telemetry is to understand usage patterns and errors in using Grace, and to then use that data to improve both functionality and performance.\n\nFor any of you who have used a telemetry provider – like Datadog, or Azure Monitor, or Application Insights, or any of a hundred others – to understand the usage of your own apps, you know what I mean. Detailed telemetry will never be kept longer than 30 days."
  },
  {
    "path": "docs/Why Auto-Rebase isn't a problem.md",
    "content": "﻿# Why Auto-Rebase isn't a problem\n\nOne of the more common feedback items I've gotten about Grace is around the idea of auto-rebasing - both positive and negative. While I agree that having auto-rebase as the default is one of the more... _progressive_ ideas in Grace, I've been surprised at some of the reaction to it.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## This has never been a thing before. Why does Grace need it?\n\nNeed is a strong word... but there are two big reasons for having auto-rebase as the default behavior in Grace.\n\nFirst, the design of [single-step branching](.\\Branching%20Strategy.md) means that a user can't promote to `main` without being rebased on the most recent promotion in `main`. Given that over 95% of rebases are a non-event, and serve only to increase quality by making sure that the code that's getting into `main` is always tested in its latest state, it's an easy choice, and will enable users to be ready to promote at all times.\n\nWhen promotion conflicts do happen, Grace's design point-of-view is that they should be dealt with immediately, and that they shouldn't wait until a PR is ready, when you get the bad news right after you think your work is done. By handling conflicts up-front, you ensure that your dev and test efforts are going to the _full, actual_ code that will be in production, and not just on the changes that you're making, separate from what the rest of your team is doing.\n\nSo, it's a branching design decision, it's a quality decision, and it's a huge part of minimizing promotion conflicts.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## What if my changes get overwritten?\nIn Grace, they won't.\n\nThe only way an auto-rebase can happen is if `grace watch` is running, and if `grace watch` is running, then the complete state of your branch was uploaded to the server the last time you saved a file, so nothing can be lost. If you need to look at the state of your branch from before the rebase, it'll be available, and you can always run `grace diff` to see what changed.\n\nIf you're editing the same file that got updated in the promotion that you're about to be auto-rebased on, you'll be alerted for a potential promotion conflict, and given the choice of how to handle it.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## What if I'm in the middle of debugging something?\nThe concern here is that you'll be in a debugging session - which, in my long but admittedly anecdotal experience, usually lasts from seconds to a few minutes - and that a promotion to `main` will happen exactly at that time, and therefore an auto-rebase will occur and one of your source code files will be updated in your working directory.\n\nBy \"debugging session\" what I mean is: debugging starts when you click on `Start debug` in your IDE, or run some command-line that starts a debugging session that you can act on, and it ends when you click `Stop Debug` or end the process you started at the command-line. Or \\<waves hands\\> something like that.\n\nFirst, is it statistically likely that you'll be debugging right when `main` gets updated? For me, in the repos that I've worked in, it's not likely, but that might not be true for you.\n\nIf you happen to run extra-long debugging sessions, and auto-rebase really would be disruptive on your machine, or on your branch, you can just stop `grace watch` on the your machine to make sure auto-rebase can't happen. Alternatively, there will be ways to [turn it off](#can-you-turn-it-off).\n\nThe IDE's that I use have an option for how to handle it when files get updated by other processes, so the first thing I think is: how does your IDE handle it? Grace is not the only process that might update a file that's open in a tab in my IDE, and I know I choose the settings in my IDE's that handle that the way that works for me.\n\nIf the file getting updated is one that you're editing, and therefore debugging, that's a promotion conflict, and you'll be notified and asked how you want to handle it.\n\nIf you're in a compiled language, it won't matter because you're debugging a compiled executable. (If you're in an interpreted language, yes, there's the possibility of some amusement.)\n\nAnd, last, let's just say that none of this helps, and, in fact, your debugging session does get messed up because of auto-rebase. Let's say that, because of your combination of languages and tools, you'll have to end debugging and restart debugging. And let's say that, for whatever reason, that happens to you often enough to be a problem. Well, you can always [turn it off](#can-you-turn-it-off). While I'm sure there are environments where it really would be disruptive, my experience and design perspective is that that will be rare.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## Can you turn it off?\n\nYes, of course. (I'm not insane.) Grace will have flags at the branch level and at the repository level to turn it off, and also in the local `graceconfig.json` if you personally don't ever want auto-rebase for whatever reason.\n\nPlease give it a chance before you do, though. You might like it more than you expect.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n\n## This is stupid, and you're stupid, and nobody wants this.\n\nParaphrasing some actual feedback I've gotten... well, 🤷‍♂️ I believe this is right design, and time will tell.\n\n![](https://gracevcsdevelopment.blob.core.windows.net/static/Orange3.svg)\n"
  },
  {
    "path": "docs/Why Grace isn't just a version control system.md",
    "content": "# Why Grace isn't just a version control system\r\n\r\n## New circumstances require new tools\r\n\r\nWhen I first created Grace, before AI coding was a thing, I worked for GitHub, and I designed it as a pure version control system. A frame I had in mind for it as I designed it was \"What belongs to Git, and what belongs to GitHub?\"\r\n\r\nGitHub already had Pull Requests, and Issues, and all of the other services and products that wrap around Git, and I didn't want to step on that. If my goal was to write something that could sit next to Git, at scale, for GitHub (and GitLab, etc.) then, I thought, it made sense to keep Grace fitting more-or-less in the same architectural box that Git sits in for large hosters.\r\n\r\nWell, if you're reading this, you're aware of how much software development has changed, and is changing, thanks to agentic coding.\r\n\r\nWhen work methods change this drastically, we have to reconsider what the right kinds of tools are, and whether the old tools are shaped correctly to meet the new requirements. And, sorry, but Git's not good enough anymore. Not even close.\r\n\r\nSteve Yegge's [Beads](https://github.com/steveyegge/beads) project, which I've used a bit for Grace, made me realize that capturing intention, and being able to track separate, small work items as part of building a feature, _alongside_ the actual code changes, was too important to leave as just-one-more-thing that we shove into Git as a file. Beads is a cute, not-exactly-designed vibe-coded hack in this direction (and I say that with love) but I think we need to have something like that as a more-deliberately-designed part of our version control now.\r\n\r\nIf you've looked into context engineering, you know that it's better for the agents to have separate, small work items. For humans reviewing that work, having everything in one place, linked together - the intention, the work item, the automated reviews, the task summaries, the diffs, all of it, linked to the exact version of the code we're interested in - gives us all of the context we need to understand what happened, and why it happened.\r\n\r\nThe fact that I'm seeing work tracking like this get built by multiple teams in multiple ways in multiple products says that it's a great candidate for including directly with the code, where all of the agents and all of the humans can find it and use it easily.\r\n\r\nAnd then I thought about how AI should fit into the creation, review, and promotion of code. Grace's event system gives us an unprecedented ability to respond to code changes in (near) real-time. What kinds of both inexpensive, determininistic code review, and costlier but more sophisticated AI reivew, should be built-in to Grace? How could we plug AI in at that level, in a way that helps resolve conflicts and enable maximum velocity? I'm still exploring that, and I'm sure I don't have all of the answers, but I know that these are crucial new kinds of primitives that deserve to be first-class constructs in a version control system now.\r\n\r\nGrace is about helping you stay calm, stay in control, and stay in flow. The more I've worked on it, the more I can't imagine not having work tracking and automated review and everything else I'm including now sitting alongside the exact code versions.\r\n\r\nI'm sure the surface area of these parts of Grace will change as we get feedback and iterate. I don't claim to have it exactly right.\r\n\r\nI do know that the minimum bar for having a useful version control system in the late 2020's has gone way, way up. Just storing the code (and only if the files are small) isn't enough anymore.\r\n"
  },
  {
    "path": "docs/Work items.md",
    "content": "# Work items\n\nWork items are durable, event-sourced records for a unit of work in Grace.\nThey capture intent (title and description), status, notes, and links to\nreferences, promotion sets, and reviewer artifacts.\n\n## Canonical command and identifier behavior\n\n- Use `grace workitem ...` as the canonical CLI path.\n- Aliases (`work`, `work-item`, `wi`) remain supported for compatibility, but\n  examples in docs should use `workitem`.\n- Work item commands that take a `work-item` argument accept either:\n  - a `WorkItemId` GUID, or\n  - a positive `WorkItemNumber` (for example `42`).\n\n## Server API surface\n\nAll work item routes are `POST` endpoints under `/work`.\n\n- `/work/create`\n- `/work/get`\n- `/work/update`\n- `/work/add-summary`\n- `/work/link/reference`\n- `/work/link/promotion-set`\n- `/work/link/artifact`\n- `/work/links/list`\n- `/work/links/remove/reference`\n- `/work/links/remove/promotion-set`\n- `/work/links/remove/artifact`\n- `/work/links/remove/artifact-type`\n- `/work/attachments/list`\n- `/work/attachments/show`\n- `/work/attachments/download`\n\n## CLI workflows\n\n### Create and inspect work items\n\nPowerShell:\n\n```powershell\n./grace workitem create `\n  --title \"Introduce baseline drift alerts\" `\n  --description \"Add baseline drift detection and update review UI\"\n\n./grace workitem create `\n  --work-item-id f88b46e2-5c36-4b52-9e36-716f7d7a9a8b `\n  --title \"Introduce baseline drift alerts\"\n\n./grace workitem show f88b46e2-5c36-4b52-9e36-716f7d7a9a8b\n./grace workitem show 42\n\n./grace workitem status f88b46e2-5c36-4b52-9e36-716f7d7a9a8b --set InReview\n./grace workitem status 42 --set Done\n```\n\nbash / zsh:\n\n```bash\n./grace workitem create \\\n  --title \"Introduce baseline drift alerts\" \\\n  --description \"Add baseline drift detection and update review UI\"\n\n./grace workitem create \\\n  --work-item-id f88b46e2-5c36-4b52-9e36-716f7d7a9a8b \\\n  --title \"Introduce baseline drift alerts\"\n\n./grace workitem show f88b46e2-5c36-4b52-9e36-716f7d7a9a8b\n./grace workitem show 42\n\n./grace workitem status f88b46e2-5c36-4b52-9e36-716f7d7a9a8b --set InReview\n./grace workitem status 42 --set Done\n```\n\n### Link references and promotion sets\n\nPowerShell:\n\n```powershell\n./grace workitem link ref `\n  f88b46e2-5c36-4b52-9e36-716f7d7a9a8b `\n  f12a0d31-0d5a-4a5f-a5a7-3d2c3a9f5b2c\n\n./grace workitem link prset `\n  42 `\n  3d5c4d9a-0123-4567-89ab-987654321000\n```\n\nbash / zsh:\n\n```bash\n./grace workitem link ref \\\n  f88b46e2-5c36-4b52-9e36-716f7d7a9a8b \\\n  f12a0d31-0d5a-4a5f-a5a7-3d2c3a9f5b2c\n\n./grace workitem link prset \\\n  42 \\\n  3d5c4d9a-0123-4567-89ab-987654321000\n```\n\n### Attach summary, prompt, and notes content\n\nPowerShell:\n\n```powershell\n./grace workitem attach summary 42 --file .\\summary.md\n./grace workitem attach prompt 42 --file .\\prompt.md\n./grace workitem attach notes 42 --text \"Reviewer follow-up required before merge.\"\n```\n\nbash / zsh:\n\n```bash\n./grace workitem attach summary 42 --file ./summary.md\n./grace workitem attach prompt 42 --file ./prompt.md\n./grace workitem attach notes 42 --text \"Reviewer follow-up required before merge.\"\n```\n\n### Retrieve reviewer attachments\n\nPowerShell:\n\n```powershell\n./grace workitem attachments list 42\n./grace workitem attachments show 42 --type summary --latest\n./grace workitem attachments download 42 `\n  --artifact-id 11111111-2222-3333-4444-555555555555 `\n  --output-file .\\summary.md\n```\n\nbash / zsh:\n\n```bash\n./grace workitem attachments list 42\n./grace workitem attachments show 42 --type summary --latest\n./grace workitem attachments download 42 \\\n  --artifact-id 11111111-2222-3333-4444-555555555555 \\\n  --output-file ./summary.md\n```\n\n### Inspect and clean up links\n\nPowerShell:\n\n```powershell\n./grace workitem links list 42\n./grace workitem links remove ref 42 f12a0d31-0d5a-4a5f-a5a7-3d2c3a9f5b2c\n./grace workitem links remove prset 42 3d5c4d9a-0123-4567-89ab-987654321000\n```\n\nbash / zsh:\n\n```bash\n./grace workitem links list 42\n./grace workitem links remove ref 42 f12a0d31-0d5a-4a5f-a5a7-3d2c3a9f5b2c\n./grace workitem links remove prset 42 3d5c4d9a-0123-4567-89ab-987654321000\n```\n\n## SDK example (F#)\n\n```fsharp\nopen Grace.SDK\nopen Grace.Shared.Parameters.WorkItem\n\nlet createParameters =\n    CreateWorkItemParameters(\n        WorkItemId = \"f88b46e2-5c36-4b52-9e36-716f7d7a9a8b\",\n        Title = \"Introduce baseline drift alerts\",\n        Description = \"Add baseline drift detection and update review UI\",\n        CorrelationId = \"corr-0001\"\n    )\n\nlet! created = WorkItem.Create(createParameters)\n\nlet linksParameters =\n    GetWorkItemLinksParameters(\n        WorkItemId = \"42\",\n        CorrelationId = \"corr-0002\"\n    )\n\nlet! links = WorkItem.GetLinks(linksParameters)\n```\n\n## Current limitations\n\n- Work item commands support reference and promotion-set links plus reviewer\n  artifact links.\n- Candidate, review packet, checkpoint, and gate-attestation link management is\n  still internal and does not yet have dedicated public work-item link\n  endpoints.\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"10.0.100\",\n    \"rollForward\": \"latestPatch\"\n  }\n}\r\n"
  },
  {
    "path": "prompts/ContentPack.prompt.md",
    "content": "You are operating inside the Grace repo as an expert .NET + F# engineer and “no vibes allowed” research agent.\r\n\r\nGOAL\r\nCreate (or update) a single Markdown artifact named `ContextPack.md` that is a *compacted, factual map of the current codebase state* relevant to the task below. This pack will be used as the ONLY context for subsequent sessions, so it must be self-contained, precise, and low-noise.\r\n\r\nTASK (fill in before running)\r\n- Bead/Issue: <bd-id or link, optional>\r\n- Topic / Change Request: <one sentence>\r\n- Constraints / Non-goals (optional): <bullet list>\r\n- Inputs: <error logs, stack traces, failing tests, links, screenshots, etc.>\r\n\r\nNON-NEGOTIABLE RULES\r\n1) This is STRICTLY a research pack: **document what exists today**.\r\n   - Do NOT propose changes.\r\n   - Do NOT critique code quality.\r\n   - Do NOT write an implementation plan.\r\n2) Every non-trivial claim MUST be backed by evidence:\r\n   - Use `path:lineStart-lineEnd` references (preferred).\r\n   - If line numbers are not available, use `path` + exact symbol names (types/functions/modules) and quote ≤ 1–2 lines.\r\n3) Prefer pointers to copies:\r\n   - Do not paste large code blocks. Keep snippets < 10 lines and only when necessary.\r\n4) Start with repo guidance:\r\n   - Read the *root* `AGENTS.md` first.\r\n   - Then locate and read any *nearest* `AGENTS.md` files under the relevant subdirectories you touch.\r\n   - Use `REPO_INDEX.md` as the jump table to find the right files fast.\r\n5) If you infer anything, label it explicitly as **INFERENCE** and list how to verify it.\r\n6) Do NOT edit code. Output only the Markdown content of `ContextPack.md`.\r\n\r\nRESEARCH METHOD (DO THIS)\r\nA) Identify likely areas/files:\r\n   - Use `REPO_INDEX.md` first.\r\n   - Then confirm with search (e.g., ripgrep) for key terms from the task, error messages, type names, endpoints, actors, etc.\r\nB) Read the smallest set of authoritative files needed to answer:\r\n   - “Where does this behavior live?”\r\n   - “What is the control/data flow?”\r\n   - “What are the contracts/invariants?”\r\n   - “What tests cover it and how do we run them?”\r\nC) Capture “similar patterns” elsewhere in the repo (for consistency).\r\n\r\nOUTPUT FORMAT (WRITE EXACTLY THIS STRUCTURE)\r\n\r\n---\r\ndate: <YYYY-MM-DD>\r\nrepo: Grace\r\nbranch: <current branch if known>\r\ncommit: <git SHA if known>\r\nbead: <bd-id if provided>\r\ntopic: \"<topic>\"\r\ntags: [contextpack, research, grace]\r\nstatus: draft|complete\r\n---\r\n\r\n# ContextPack: <topic>\r\n\r\n## 1. Research question\r\n- <What exactly are we trying to understand about the current system?>\r\n\r\n## 2. Scope\r\n- In-scope:\r\n  - ...\r\n- Out-of-scope:\r\n  - ...\r\n\r\n## 3. Key findings (TL;DR)\r\n- 5–12 bullets, each with evidence references.\r\n- Focus on “how it works today” and “where to look.”\r\n\r\n## 4. File map (authoritative sources)\r\nList the minimal set of files to understand the area, each with:\r\n- Path\r\n- Why it matters\r\n- Key symbols inside (types/functions/modules)\r\n- Evidence refs\r\n\r\nExample:\r\n- `src/Grace.Server/Foo.fs:120-260`\r\n  - Why: request handler for X\r\n  - Symbols: `FooHandler.handle`, `FooDto`\r\n  - Notes: ...\r\n\r\n## 5. Current behavior (flows)\r\nDescribe the current runtime behavior as sequences.\r\nUse headings like:\r\n- “Inbound request → … → side effects”\r\n- “Command → actor → persistence → reply”\r\n- “CLI → SDK → server → response”\r\nEach step should include file:line evidence.\r\n\r\n## 6. Data contracts and invariants\r\n- Important types, schemas, discriminated unions, DTOs, serialization formats\r\n- Invariants that *must* hold\r\n- Versioning/back-compat constraints\r\nAll with evidence.\r\n\r\n## 7. Cross-project implications\r\nCall out implications across:\r\n- Grace.Types\r\n- Grace.Shared\r\n- Grace.Server\r\n- Grace.Actors\r\n- Grace.CLI\r\n- Grace.SDK\r\nOnly include projects actually implicated (with evidence).\r\n\r\n## 8. Test and verification surface\r\n- Existing tests relevant to this area (paths + what they cover)\r\n- How to run them (commands)\r\n- Any missing tests you notice ONLY as “coverage gaps” (no solutions, no critique)\r\n\r\n## 9. Open questions / unknowns\r\n- Questions that must be answered before planning\r\n- For each: how to verify (file to read, command to run, runtime probe, etc.)\r\n\r\n## 10. Appendix: search terms and breadcrumbs\r\n- Exact strings searched (error messages, identifiers)\r\n- Useful ripgrep patterns\r\n- Any relevant commits/PRs (if provided)\r\n\r\nQUALITY BAR\r\n- Keep it tight: target 150–300 lines unless the area is genuinely large.\r\n- This document must be “drop-in context” for a fresh Plan session.\r\n\r\nNow produce ONLY the full Markdown content for `ContextPack.md`.\r\n"
  },
  {
    "path": "prompts/Grace issue summary.md",
    "content": "# Grace Issue Summary Prompt\r\n\r\nUse this prompt to produce a complete GitHub issue body for the Grace repository.\r\n\r\n## Role\r\n\r\nYou are preparing a high-rigor issue write-up that will be reviewed by humans and re-researched by other LLMs.\r\nWrite clearly, concretely, and thoroughly.\r\n\r\n## Compliance Gate\r\n\r\nBefore writing, confirm and report:\r\n\r\n1. Harness used.\r\n2. Exact model used.\r\n3. Reasoning or effort level used.\r\n4. Whether this run used the latest generally available model from that provider.\r\n5. Whether reasoning is at least equivalent to OpenAI `high`.\r\n\r\nIf items 4 or 5 are not true, stop and output only:\r\n`NON-COMPLIANT: latest-model and high-reasoning requirements not met.`\r\n\r\n## Runtime Metadata Script (Required)\r\n\r\nBefore drafting the issue body, run the metadata collection script and use its output as the source of truth for:\r\n`harness`, `provider`, `model`, `reasoning_level`, `reasoning_level_equivalent`, `high_reasoning_asserted`,\r\n`latest_model_asserted`, `metadata_source`, and `metadata_evidence`.\r\n\r\nPowerShell:\r\n\r\n```powershell\r\npwsh ./scripts/collect-runtime-metadata.ps1 -WorkspacePath <repo-root> -OutputFormat Yaml\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\npwsh ./scripts/collect-runtime-metadata.ps1 -WorkspacePath <repo-root> -OutputFormat Yaml\r\n```\r\n\r\nFor auditability, prefer a second run with `-Verbose` and capture important discovery notes in section 3 (Prompt Log).\r\n\r\n## Copy/Paste Snippet\r\n\r\nUse one of these commands to print a ready-to-paste YAML block for the top of the issue body.\r\nAfter pasting, add `prompt_count: <integer>` on its own line inside the same YAML block.\r\n\r\nPowerShell:\r\n\r\n```powershell\r\n$meta = pwsh ./scripts/collect-runtime-metadata.ps1 -WorkspacePath . -OutputFormat Yaml\r\n$metaText = $meta -join [Environment]::NewLine\r\n(\r\n  '```yaml' + [Environment]::NewLine + $metaText + [Environment]::NewLine +\r\n  'prompt_count: <integer>' + [Environment]::NewLine + '```'\r\n)\r\n```\r\n\r\nbash / zsh:\r\n\r\n```bash\r\nmeta=\"$(pwsh ./scripts/collect-runtime-metadata.ps1 -WorkspacePath . -OutputFormat Yaml)\"\r\nprintf '```yaml\\n%s\\nprompt_count: <integer>\\n```\\n' \"$meta\"\r\n```\r\n\r\n## Runtime Metadata Discovery (Required)\r\n\r\nCollect run metadata before drafting the issue body.\r\nUse this discovery order and stop at the first authoritative value found for each field\r\n(`harness`, `provider`, `model`, `reasoning_level`):\r\n\r\n1. Session/runtime status output exposed by the harness\r\n   (for example `/status`, status line, run header, structured run metadata).\r\n2. Effective runtime settings shown by the harness (active profile, launch flags, resolved settings).\r\n3. Harness configuration files in user home and repository/workspace scopes.\r\n4. Environment variables commonly used for model selection.\r\n5. If still unknown: set value to `unknown` and explain exactly what was checked.\r\n\r\nWhen reading config files, prefer keys like:\r\n\r\n- `model`, `default_model`, `model_name`, `engine`, `deployment`, `reasoning_effort`, `reasoning`, `thinking`, `effort`\r\n- profile-scoped overrides that supersede global defaults\r\n\r\nAlways include a short evidence line for each discovered field. Evidence must be:\r\n\r\n- a file path and key name, or\r\n- a runtime status field name\r\n\r\nDo not claim a value without evidence.\r\n\r\n## Reasoning-Level Normalization (Required)\r\n\r\nSet `reasoning_level` to the provider-native setting (verbatim when available). Then compute\r\n`reasoning_level_equivalent` as one of: `low`, `medium`, `high`, `xhigh`.\r\n\r\nNormalization rules (use the first rule that applies):\r\n\r\n1. If provider explicitly reports one of `low|medium|high|xhigh`, use it directly.\r\n2. If provider reports text containing:\r\n    - `minimal`, `light`, `fast` => `low`\r\n    - `balanced`, `standard`, `normal`, `default` => `medium`\r\n    - `deep`, `intensive`, `high` => `high`\r\n    - `max`, `very-high`, `ultra`, `extended` => `xhigh`\r\n3. If reasoning/thinking is boolean only:\r\n    - disabled/off => `low`\r\n    - enabled/on with no strength/budget detail => `medium` (conservative default)\r\n4. If a numeric reasoning budget is available (tokens/steps):\r\n    - `<= 2000` => `low`\r\n    - `2001-8000` => `medium`\r\n    - `8001-24000` => `high`\r\n    - `> 24000` => `xhigh`\r\n5. If none apply => `unknown` and mark compliance as false.\r\n\r\n## Latest-Model Assertion Rule (Required)\r\n\r\nSet `latest_model_asserted: true` only when you have explicit evidence from this same run that the selected model is the\r\nlatest generally available model for that provider.\r\n\r\nAcceptable evidence:\r\n\r\n- harness-provided statement that the active model is latest/current, or\r\n- a provider source checked in-run with date and link\r\n\r\nIf that evidence is missing, set `latest_model_asserted: false`. Do not guess.\r\n\r\n## Input You Should Receive\r\n\r\n- Issue topic or problem statement.\r\n- Relevant repository path or subsystem (if known).\r\n- Any logs, stack traces, user reports, or screenshots.\r\n\r\nIf inputs are missing, proceed with best-effort repo research and explicitly list assumptions.\r\n\r\n## Research Requirements\r\n\r\n1. Inspect the repository before drafting recommendations.\r\n2. Cite concrete evidence in the issue body.\r\n3. Distinguish evidence from inference.\r\n4. Prefer specific findings over general opinions.\r\n\r\nFor evidence, include:\r\n\r\n- File paths.\r\n- Symbols (types/functions/modules/classes).\r\n- Behavioral observations.\r\n\r\nUse evidence anchors for every major claim in sections 4 and 5.\r\n\r\nAnchor format:\r\n\r\n- Preferred: `path:line` or `path:startLine-endLine`\r\n- Acceptable when lines are unavailable: `path` + symbol name\r\n\r\nIf a claim cannot be anchored, label it `INFERENCE` and include a one-line verification plan.\r\n\r\n## Required Output Format\r\n\r\nOutput only Markdown for the issue body, using this exact section structure and headings.\r\n\r\n### 0) Machine-Readable Metadata\r\n\r\nPopulate YAML metadata fields from `collect-runtime-metadata.ps1` output. Only `prompt_count` may be computed separately.\r\n\r\nStart the issue body with this YAML block:\r\n\r\n```yaml\r\nharness: <tool/harness name>\r\nprovider: <model provider>\r\nmodel: <exact model identifier>\r\nreasoning_level: <provider-specific setting>\r\nreasoning_level_equivalent: <OpenAI low|medium|high|xhigh equivalent>\r\nlatest_model_asserted: true|false\r\nhigh_reasoning_asserted: true|false\r\nprompt_count: <integer>\r\nmetadata_source: <status|runtime-settings|config-file|env|mixed|unknown>\r\nmetadata_evidence:\r\n  harness: <short evidence>\r\n  provider: <short evidence>\r\n  model: <short evidence>\r\n  reasoning_level: <short evidence>\r\ngenerated_at_utc: <ISO-8601 UTC timestamp>\r\n```\r\n\r\n### 1) Submission Metadata\r\n\r\n- Harness:\r\n- Provider:\r\n- Model:\r\n- Reasoning Level:\r\n- Reasoning Level Equivalent:\r\n- Latest-Model Compliance:\r\n- High-Reasoning Compliance:\r\n- Metadata Source:\r\n- Metadata Evidence:\r\n- Timestamp (UTC):\r\n\r\n### 2) Issue Introduction\r\n\r\nProvide a concise introduction that explains:\r\n\r\n- What the issue is.\r\n- Why it matters.\r\n- Who or what is affected.\r\n\r\n### 3) Prompt Log\r\n\r\nList every meaningful prompt used to produce this issue. Redact all secret values from the prompt.\r\nInclude the exact metadata script command(s) that were run before repo research.\r\n\r\nFor each prompt, include:\r\n\r\n- Prompt ID (P1, P2, ...)\r\n- Purpose\r\n- Prompt text\r\n- Notable impact on findings (1-2 bullets)\r\n\r\n### 4) Repository Research Summary\r\n\r\nProvide a detailed rundown of the repo research performed.\r\n\r\nInclude:\r\n\r\n- Paths reviewed\r\n- Key findings per path\r\n- Current behavior as implemented today\r\n- Evidence vs inference labels where needed\r\n- Evidence anchors for each major claim\r\n\r\n### 5) Recommendations\r\n\r\nBreak recommendations into these subsections.\r\n\r\n#### 5.1 Feature-Level Updates\r\n\r\n- What should change in behavior or UX.\r\n- Expected outcomes.\r\n- Evidence anchors for each major recommendation.\r\n\r\n#### 5.2 Documentation Updates\r\n\r\n- Which docs should be updated.\r\n- What new guidance or corrections should be added.\r\n- Evidence anchors for each major recommendation.\r\n\r\n#### 5.3 Code-Level Updates\r\n\r\n- Candidate files/components to modify.\r\n- Suggested implementation direction.\r\n- Test coverage additions or updates needed.\r\n- Evidence anchors for each major recommendation.\r\n\r\n### 6) Risks and Unknowns\r\n\r\n- Technical risks.\r\n- Assumptions.\r\n- Open questions requiring maintainer input.\r\n\r\n### 7) Proposed Acceptance Criteria\r\n\r\nProvide a concrete checklist that maintainers can use to verify completion.\r\n\r\n### 8) Human Accountability\r\n\r\nEnd with this exact checklist item:\r\n\r\n- [ ] I have personally reviewed this AI-assisted issue summary, verified the evidence anchors, and I stand behind it.\r\n\r\n## Writing Constraints\r\n\r\n- Be explicit, not generic.\r\n- Prefer bullet lists with concrete details.\r\n- Keep claims falsifiable and reviewable.\r\n- Assume this text will be audited by additional LLMs, so optimize for clarity and traceability.\r\n"
  },
  {
    "path": "prompts/Grace pull request summary.md",
    "content": "# Grace PR Summary Prompt\n\nUse this prompt to produce a complete GitHub pull request body for the Grace repository.\n\n## Role\n\nYou are preparing a high-rigor PR summary that will be reviewed by humans and re-researched by other LLMs.\nWrite clearly, concretely, and thoroughly.\n\n## Compliance Gate\n\nBefore writing, confirm and report:\n\n1. Harness used.\n2. Exact model used.\n3. Reasoning or effort level used.\n4. Whether this run used the latest generally available model from that provider.\n5. Whether reasoning / effort is at least equivalent to Codex and Claude's `high`.\n\nIf items 4 or 5 are not true, stop and output only:\n`NON-COMPLIANT: latest-model and high-reasoning requirements not met.`\n\n## Input You Should Receive\n\n- Branch or diff to summarize.\n- Work item or issue reference (if applicable).\n- Validation results (tests/build/lint/manual checks).\n\nIf any input is unavailable, state that explicitly and continue with what is verifiable.\n\n## Analysis Requirements\n\n1. Inspect the actual diff and changed files.\n2. Summarize behavior-level change, not just line edits.\n3. Map each changed file to what was accomplished.\n4. Include evidence from validation results.\n\nUse evidence anchors for every major claim in sections 4 through 7.\n\nAnchor format:\n\n- Preferred: `path:line` or `path:startLine-endLine`\n- Acceptable when lines are unavailable: `path` + symbol name\n\nIf a claim cannot be anchored, label it `INFERENCE` and include a one-line verification plan.\n\n## Required Output Format\n\nOutput only Markdown for the PR body, using this exact section structure and headings.\n\n### 0) Machine-Readable Metadata\n\nStart the PR body with this YAML block:\n\n```yaml\nharness: <tool/harness name>\nprovider: <model provider>\nmodel: <exact model identifier>\nreasoning_level: <provider-specific setting>\nreasoning_level_equivalent: <OpenAI low|medium|high|xhigh equivalent>\nlatest_model_asserted: true|false\nhigh_reasoning_asserted: true|false\nprompt_count: <integer>\ngenerated_at_utc: <ISO-8601 UTC timestamp>\n```\n\n### 1) Submission Metadata\n\n- Harness:\n- Provider:\n- Model:\n- Reasoning Level:\n- Reasoning Level Equivalent:\n- Latest-Model Compliance:\n- High-Reasoning Compliance:\n- Timestamp (UTC):\n\n### 2) PR Introduction\n\nProvide an introduction that summarizes:\n\n- Overall purpose of the change.\n- Related work item/issue (if applicable).\n- Why this change is needed now.\n\n### 3) Prompt Log\n\nList every meaningful prompt used to produce this PR summary. Redact all secret values from the prompt.\n\nFor each prompt, include:\n\n- Prompt ID (P1, P2, ...)\n- Purpose\n- Prompt text\n- Notable impact on outcome (1-2 bullets)\n\n### 4) Features Changed and Bugs Fixed\n\nProvide a clear bullet list of:\n\n- Features added or changed.\n- Bugs fixed.\n- Any behavior intentionally unchanged.\n- Evidence anchors for each major claim.\n\n### 5) File-by-File Change Summary\n\nFor each changed file, include:\n\n- File path\n- What changed\n- Why that change was necessary\n- Evidence anchor\n\n### 6) Validation and Verification\n\nInclude exactly what was run and what happened:\n\n- Build/test/lint commands\n- Manual verification steps\n- Results and any known gaps\n- Evidence anchors for each major claim\n\n### 7) Risks, Compatibility, and Follow-Ups\n\n- Risks introduced or reduced\n- Backward compatibility notes\n- Follow-up work or deferred items\n- Evidence anchors for each major claim\n\n### 8) Human Accountability\n\nEnd with this exact checklist item:\n\n- [ ] I have personally reviewed this AI-assisted PR summary, verified the evidence anchors, and I stand behind it.\n\n## Writing Constraints\n\n- Be explicit, not generic.\n- Avoid vague statements like \"minor updates\".\n- Prefer concrete, reviewer-friendly language.\n- Assume this text will be audited by additional LLMs, so optimize for clarity and traceability.\n"
  },
  {
    "path": "prompts/PlanPack.prompt.md",
    "content": "You are operating inside the Grace repo as an expert .NET + F# engineer and planning/spec agent.\r\n\r\nGOAL\r\nCreate (or update) `PlanPack.md`: a *phased, test-first implementation plan* that is executable by an AI coding agent with high reliability, based on `ContextPack.md`.\r\n\r\nINPUTS (fill in before running)\r\n- ContextPack path or pasted content: <required>\r\n- Bead/Issue: <bd-id or link, optional>\r\n- Desired end state: <bullets>\r\n- Non-goals / guardrails: <bullets>\r\n- Constraints: <perf, compatibility, security, timelines, etc.>\r\n\r\nNON-NEGOTIABLE RULES\r\n1) Do NOT implement anything. Do NOT modify code. Output only `PlanPack.md`.\r\n2) The plan must be **phased** with checkboxes and explicit verification after each phase.\r\n3) Prefer deterministic tooling over LLM effort:\r\n   - Formatting/linting should be run by tools (e.g., fantomas), not “mentally simulated.”\r\n4) Provide options when the design choice is non-obvious.\r\n5) Every phase must specify:\r\n   - Files to edit (paths)\r\n   - What to change (concise)\r\n   - Tests/verification commands (exact)\r\n   - Success criteria (objective)\r\n6) If any key decision is missing, ask up to 5 clarifying questions FIRST.\r\n   - If answers aren’t available, proceed with explicit “Assumptions” clearly labeled.\r\n\r\nPLANNING METHOD (DO THIS)\r\nA) Read `ContextPack.md` thoroughly.\r\nB) Extract:\r\n   - Current state summary\r\n   - Invariants\r\n   - Integration points\r\n   - Existing tests\r\nC) Produce:\r\n   - Design options (if any)\r\n   - Recommended approach with rationale\r\n   - Phased implementation plan with checkpoints\r\n   - Beads-aligned work plan using `bd` commands\r\n\r\nOUTPUT FORMAT (WRITE EXACTLY THIS STRUCTURE)\r\n\r\n---\r\ndate: <YYYY-MM-DD>\r\nrepo: Grace\r\nbranch: <current branch if known>\r\ncommit: <git SHA if known>\r\nbead: <bd-id if provided>\r\ntopic: \"<topic>\"\r\ninputs:\r\n  - ContextPack.md\r\ntags: [planpack, plan, rpi, grace]\r\nstatus: draft|final\r\n---\r\n\r\n# PlanPack: <topic>\r\n\r\n## 1. Goal and success criteria\r\n- Goal:\r\n- Definition of Done (DoD):\r\n  - [ ] All tests pass (`dotnet test --no-build`)\r\n  - [ ] Build passes (`dotnet build --configuration Release`)\r\n  - [ ] F# formatted if touched (`fantomas .`)\r\n  - [ ] No new warnings (or explicitly justified)\r\n  - [ ] Behavioral verification steps completed (list)\r\n\r\n## 2. Constraints and non-goals\r\n- Constraints:\r\n- Non-goals:\r\n\r\n## 3. Clarifying questions (if needed)\r\n- Q1 …\r\n- Q2 …\r\n\r\n## 4. Options (only if real trade-offs exist)\r\nFor each option:\r\n- Summary\r\n- Pros/cons\r\n- Risks\r\n- Decision impact\r\n- Recommendation\r\n\r\n## 5. Proposed design\r\n- Key design decisions\r\n- Public contracts affected (types/APIs/config)\r\n- Backwards-compat strategy\r\n- Error handling and logging expectations (no secrets/PII; preserve correlation IDs)\r\n- Test strategy overview\r\n\r\n## 6. Implementation plan (phased)\r\nWrite phases as a checklist. Each phase must be small enough to review and verify.\r\n\r\n### Phase 0 — Safety setup (if needed)\r\n- [ ] Create branch / ensure clean status\r\n- [ ] Baseline verify: run existing tests/build\r\n- Verification:\r\n  - `dotnet build --configuration Release`\r\n  - `dotnet test --no-build`\r\n- Success criteria:\r\n  - ...\r\n\r\n### Phase 1 — <name>\r\n- [ ] Edit files:\r\n  - `path/to/file1` — <what to change>\r\n  - `path/to/file2` — <what to change>\r\n- [ ] Add/adjust tests:\r\n  - `path/to/test` — <what to cover>\r\n- Verification (run *after* this phase):\r\n  - <exact commands>\r\n- Success criteria:\r\n  - <objective outcomes>\r\n- Notes / pitfalls:\r\n  - <edge cases, invariants>\r\n\r\n(repeat for all phases)\r\n\r\n## 7. Verification matrix\r\nA table-like list (no actual markdown table required) mapping:\r\n- Risk area → automated checks → manual checks → evidence\r\n\r\n## 8. Beads Work Plan (bd)\r\nDecompose into beads-aligned work items with completion definitions and suggested commands, e.g.:\r\n- Work item A: <definition of done>\r\n  - Suggested commands:\r\n    - `bd create ...`\r\n    - `bd ready <id>`\r\n    - `bd close <id>`\r\n- Work item B: ...\r\n\r\n## 9. Rollback / recovery\r\n- How to revert safely (git strategy)\r\n- Feature flags / config toggles (if applicable)\r\n- Migration rollback (if applicable)\r\n\r\n## 10. Handoff expectations\r\n- What the eventual `HandoffPack.md` must report (tests run, files changed, decisions, remaining work)\r\n\r\nQUALITY BAR\r\n- Make it executable: unambiguous steps, explicit commands, explicit file targets.\r\n- Keep it readable: prefer bullets, short paragraphs, and checklists.\r\n\r\nNow produce ONLY the full Markdown content for `PlanPack.md`.\r\n"
  },
  {
    "path": "scripts/bootstrap.ps1",
    "content": "[CmdletBinding()]\nparam(\n    [switch]$SkipDocker,\n    [switch]$CI\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\nif ($CI) {\n    $ProgressPreference = \"SilentlyContinue\"\n}\n\n$startTime = Get-Date\n$exitCode = 0\n\nfunction Write-Section([string]$Title) {\n    Write-Host \"\"\n    Write-Host (\"== {0} ==\" -f $Title)\n}\n\nfunction Invoke-External([string]$Label, [scriptblock]$Command) {\n    & $Command\n    if ($LASTEXITCODE -ne 0) {\n        throw \"$Label failed with exit code $LASTEXITCODE.\"\n    }\n}\n\ntry {\n    Write-Section \"Prerequisites\"\n\n    if ($PSVersionTable.PSVersion.Major -lt 7) {\n        throw \"PowerShell 7.x is required. Current version: $($PSVersionTable.PSVersion).\"\n    }\n\n    Write-Verbose (\"PowerShell version: {0}\" -f $PSVersionTable.PSVersion)\n\n    try {\n        Get-Command dotnet -ErrorAction Stop | Out-Null\n    } catch {\n        throw \"dotnet SDK not found. Install the .NET 10 SDK and retry.\"\n    }\n\n    $dotnetVersion = (& dotnet --version).Trim()\n    if ($LASTEXITCODE -ne 0) {\n        throw \"dotnet --version failed. Ensure the .NET 10 SDK is installed.\"\n    }\n    if ([string]::IsNullOrWhiteSpace($dotnetVersion)) {\n        throw \"dotnet --version returned an empty value.\"\n    }\n\n    $dotnetMajor = [int]($dotnetVersion.Split(\".\")[0])\n    if ($dotnetMajor -lt 10) {\n        throw \"dotnet SDK 10.x required. Detected $dotnetVersion.\"\n    }\n\n    if (-not $SkipDocker) {\n        try {\n            Get-Command docker -ErrorAction Stop | Out-Null\n        } catch {\n            throw \"Docker is required for full validation. Install Docker Desktop or run with -SkipDocker.\"\n        }\n\n        Invoke-External \"docker info\" { docker info | Out-Null }\n    } else {\n        Write-Host \"Skipping Docker check (-SkipDocker).\"\n    }\n\n    Write-Section \"Restore Tools\"\n    Invoke-External \"dotnet tool restore\" { dotnet tool restore }\n\n    Write-Section \"Restore Packages\"\n    Invoke-External \"dotnet restore\" { dotnet restore \"src/Grace.sln\" }\n\n    Write-Section \"Next Steps\"\n    Write-Host \"Run: pwsh ./scripts/validate.ps1 -Fast\"\n    Write-Host \"Use -Full for Aspire integration coverage.\"\n} catch {\n    $exitCode = 1\n    Write-Error $_\n} finally {\n    $elapsed = (Get-Date) - $startTime\n    Write-Host \"\"\n    Write-Host (\"Elapsed: {0:c}\" -f $elapsed)\n\n    if ($exitCode -ne 0) {\n        exit $exitCode\n    }\n}\n"
  },
  {
    "path": "scripts/collect-runtime-metadata.ps1",
    "content": "[CmdletBinding()]\nparam(\n    [string]$WorkspacePath = (Get-Location).Path,\n    [string]$StatusText,\n    [string]$StatusFile,\n    [ValidateSet(\"Object\", \"Json\", \"Yaml\")]\n    [string]$OutputFormat = \"Json\",\n    [string]$OutputPath\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\nfunction New-FieldRecord {\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Name\n    )\n\n    [ordered]@{\n        name = $Name\n        value = \"unknown\"\n        source = \"unknown\"\n        evidence = \"not found\"\n        confidence = \"low\"\n    }\n}\n\nfunction Set-FieldValue {\n    param(\n        [Parameter(Mandatory = $true)]\n        [System.Collections.IDictionary]$Field,\n        [Parameter(Mandatory = $true)]\n        [AllowEmptyString()]\n        [string]$Value,\n        [Parameter(Mandatory = $true)]\n        [string]$Source,\n        [Parameter(Mandatory = $true)]\n        [string]$Evidence,\n        [ValidateSet(\"low\", \"medium\", \"high\")]\n        [string]$Confidence = \"medium\",\n        [switch]$Force\n    )\n\n    if (-not $Force -and $Field.value -ne \"unknown\" -and -not [string]::IsNullOrWhiteSpace($Field.value)) {\n        Write-Verbose (\"Skipping update for [{0}] because a value already exists from [{1}].\" -f $Field.name, $Field.source)\n        return\n    }\n\n    $normalizedValue =\n        if ([string]::IsNullOrWhiteSpace($Value)) {\n            \"unknown\"\n        } else {\n            $Value.Trim()\n        }\n\n    $Field.value = $normalizedValue\n    $Field.source = $Source\n    $Field.evidence = $Evidence\n    $Field.confidence = $Confidence\n\n    Write-Verbose (\n        \"Captured field [{0}] = [{1}] from [{2}] (confidence: {3}). Evidence: {4}\" -f\n        $Field.name,\n        $Field.value,\n        $Field.source,\n        $Field.confidence,\n        $Field.evidence\n    )\n}\n\nfunction Resolve-TextValue {\n    param(\n        [AllowNull()]\n        [object]$Value\n    )\n\n    if ($null -eq $Value) {\n        return $null\n    }\n\n    if ($Value -is [string]) {\n        $text = $Value.Trim().Trim(\"'`\"\")\n        if ([string]::IsNullOrWhiteSpace($text)) {\n            return $null\n        }\n\n        return $text\n    }\n\n    return ([string]$Value).Trim()\n}\n\nfunction Get-ProcessAncestors {\n    $ancestors = New-Object System.Collections.Generic.List[object]\n\n    try {\n        $process = Get-CimInstance Win32_Process -Filter (\"ProcessId={0}\" -f $PID)\n    } catch {\n        Write-Verbose (\"Unable to inspect process tree: {0}\" -f $_.Exception.Message)\n        return @()\n    }\n\n    $depth = 0\n    while ($null -ne $process -and $depth -lt 15) {\n        $record = [pscustomobject]@{\n            depth = $depth\n            name = $process.Name\n            pid = $process.ProcessId\n            parentPid = $process.ParentProcessId\n            commandLine = $process.CommandLine\n        }\n        $ancestors.Add($record)\n\n        Write-Verbose (\n            \"Process ancestor depth={0} name={1} pid={2} parentPid={3}\" -f\n            $record.depth,\n            $record.name,\n            $record.pid,\n            $record.parentPid\n        )\n\n        if (-not $process.ParentProcessId) {\n            break\n        }\n\n        try {\n            $process = Get-CimInstance Win32_Process -Filter (\"ProcessId={0}\" -f $process.ParentProcessId)\n        } catch {\n            break\n        }\n        $depth++\n    }\n\n    return $ancestors\n}\n\nfunction Get-HarnessFromProcessTree {\n    param(\n        [Parameter(Mandatory = $true)]\n        [object[]]$Ancestors\n    )\n\n    $patterns = @(\n        @{ Regex = \"codex\"; Harness = \"codex\"; Confidence = \"high\" },\n        @{ Regex = \"claude\"; Harness = \"claude\"; Confidence = \"high\" },\n        @{ Regex = \"cursor\"; Harness = \"cursor\"; Confidence = \"high\" },\n        @{ Regex = \"copilot\"; Harness = \"copilot\"; Confidence = \"high\" },\n        @{ Regex = \"gemini\"; Harness = \"gemini\"; Confidence = \"high\" }\n    )\n\n    foreach ($ancestor in $Ancestors) {\n        $name = [string]$ancestor.name\n        $cmd = [string]$ancestor.commandLine\n        foreach ($pattern in $patterns) {\n            if ($name -match $pattern.Regex -or $cmd -match $pattern.Regex) {\n                return [ordered]@{\n                    harness = $pattern.Harness\n                    evidence = (\"process tree: {0} (pid {1})\" -f $ancestor.name, $ancestor.pid)\n                    confidence = $pattern.Confidence\n                }\n            }\n        }\n    }\n\n    return $null\n}\n\nfunction Get-HarnessFromEnvironment {\n    $envCandidates = @(\n        @{ Name = \"CODEX_THREAD_ID\"; Harness = \"codex\" },\n        @{ Name = \"CODEX_MANAGED_BY_NPM\"; Harness = \"codex\" },\n        @{ Name = \"CLAUDE_CODE\"; Harness = \"claude\" },\n        @{ Name = \"CURSOR_TRACE_ID\"; Harness = \"cursor\" },\n        @{ Name = \"GITHUB_COPILOT_TOKEN\"; Harness = \"copilot\" },\n        @{ Name = \"GEMINI_API_KEY\"; Harness = \"gemini\" }\n    )\n\n    foreach ($candidate in $envCandidates) {\n        $value = [Environment]::GetEnvironmentVariable($candidate.Name)\n        if (-not [string]::IsNullOrWhiteSpace($value)) {\n            return [ordered]@{\n                harness = $candidate.Harness\n                evidence = (\"environment variable: {0}\" -f $candidate.Name)\n                confidence = \"medium\"\n            }\n        }\n    }\n\n    return $null\n}\n\nfunction Get-ProviderFromModel {\n    param(\n        [string]$Model\n    )\n\n    if ([string]::IsNullOrWhiteSpace($Model) -or $Model -eq \"unknown\") {\n        return $null\n    }\n\n    $normalized = $Model.ToLowerInvariant()\n    $mappings = @(\n        @{ Pattern = \"^(gpt|o[1-9]|chatgpt|gpt-5|gpt-4|o3|o4|gpt-5\\.)\"; Provider = \"openai\" },\n        @{ Pattern = \"codex\"; Provider = \"openai\" },\n        @{ Pattern = \"^claude\"; Provider = \"anthropic\" },\n        @{ Pattern = \"^gemini\"; Provider = \"google\" },\n        @{ Pattern = \"command-r|cohere\"; Provider = \"cohere\" },\n        @{ Pattern = \"^mistral\"; Provider = \"mistral\" },\n        @{ Pattern = \"^deepseek\"; Provider = \"deepseek\" },\n        @{ Pattern = \"^qwen\"; Provider = \"alibaba\" },\n        @{ Pattern = \"^grok\"; Provider = \"xai\" },\n        @{ Pattern = \"llama\"; Provider = \"meta-or-compatible\" },\n        @{ Pattern = \"^phi\"; Provider = \"microsoft\" }\n    )\n\n    foreach ($map in $mappings) {\n        if ($normalized -match $map.Pattern) {\n            return $map.Provider\n        }\n    }\n\n    return \"unknown\"\n}\n\nfunction Get-ProviderFromHarness {\n    param(\n        [string]$Harness\n    )\n\n    switch ($Harness) {\n        \"codex\" { return \"openai\" }\n        \"claude\" { return \"anthropic\" }\n        \"gemini\" { return \"google\" }\n        default { return \"unknown\" }\n    }\n}\n\nfunction Convert-ToReasoningEquivalent {\n    param(\n        [string]$ReasoningLevel\n    )\n\n    if ([string]::IsNullOrWhiteSpace($ReasoningLevel) -or $ReasoningLevel -eq \"unknown\") {\n        return \"unknown\"\n    }\n\n    $normalized = $ReasoningLevel.Trim().ToLowerInvariant()\n\n    if ($normalized -in @(\"low\", \"medium\", \"high\", \"xhigh\")) {\n        return $normalized\n    }\n\n    if ($normalized -in @(\"false\", \"off\", \"disabled\", \"none\")) {\n        return \"low\"\n    }\n\n    if ($normalized -in @(\"true\", \"on\", \"enabled\")) {\n        return \"medium\"\n    }\n\n    if ($normalized -match \"\\b(minimal|light|fast|quick|low)\\b\") {\n        return \"low\"\n    }\n\n    if ($normalized -match \"\\b(balanced|standard|normal|default|medium)\\b\") {\n        return \"medium\"\n    }\n\n    if ($normalized -match \"\\b(deep|intensive|high)\\b\") {\n        return \"high\"\n    }\n\n    if ($normalized -match \"\\b(max|very-high|ultra|extended|xhigh)\\b\") {\n        return \"xhigh\"\n    }\n\n    if ($normalized -match \"^\\d+$\") {\n        $budget = [int]$normalized\n        if ($budget -le 2000) {\n            return \"low\"\n        }\n\n        if ($budget -le 8000) {\n            return \"medium\"\n        }\n\n        if ($budget -le 24000) {\n            return \"high\"\n        }\n\n        return \"xhigh\"\n    }\n\n    return \"unknown\"\n}\n\nfunction Get-CandidateConfigPaths {\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$WorkspaceRoot\n    )\n\n    $homeDir = [Environment]::GetFolderPath(\"UserProfile\")\n    $appData = [Environment]::GetFolderPath(\"ApplicationData\")\n    $localAppData = [Environment]::GetFolderPath(\"LocalApplicationData\")\n\n    $candidates = New-Object System.Collections.Generic.List[string]\n    $known = @(\n        \"$homeDir\\.codex\\config.toml\",\n        \"$homeDir\\.claude\\settings.json\",\n        \"$homeDir\\.claude\\config.json\",\n        \"$homeDir\\.gemini\\settings.json\",\n        \"$homeDir\\.gemini\\config.json\",\n        \"$homeDir\\.copilot\\config.json\",\n        \"$homeDir\\.config\\codex\\config.toml\",\n        \"$homeDir\\.config\\claude\\settings.json\",\n        \"$homeDir\\.config\\gemini\\config.json\",\n        \"$homeDir\\.config\\copilot\\config.json\",\n        \"$homeDir\\.config\\github-copilot\\config.json\",\n        \"$homeDir\\.config\\cursor\\settings.json\",\n        \"$appData\\Cursor\\User\\settings.json\",\n        \"$localAppData\\Programs\\cursor\\resources\\app\\settings.json\",\n        \"$WorkspaceRoot\\.codex\\config.toml\",\n        \"$WorkspaceRoot\\.claude\\settings.json\",\n        \"$WorkspaceRoot\\.gemini\\settings.json\",\n        \"$WorkspaceRoot\\.copilot\\config.json\",\n        \"$WorkspaceRoot\\.cursor\\settings.json\",\n        \"$WorkspaceRoot\\.vscode\\settings.json\"\n    )\n\n    foreach ($path in $known) {\n        if ([string]::IsNullOrWhiteSpace($path)) {\n            continue\n        }\n\n        if ($candidates.Contains($path)) {\n            continue\n        }\n\n        $candidates.Add($path)\n    }\n\n    return $candidates\n}\n\nfunction Get-KeyMatchFromText {\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Text,\n        [Parameter(Mandatory = $true)]\n        [string[]]$Keys\n    )\n\n    foreach ($key in $Keys) {\n        $pattern = \"(?im)^\\s*[\"\"'']?{0}[\"\"'']?\\s*[:=]\\s*[\"\"'']?(?<value>[^`r`n#,\"\"']+)\" -f [Regex]::Escape($key)\n        $match = [Regex]::Match($Text, $pattern)\n        if ($match.Success) {\n            $value = Resolve-TextValue -Value $match.Groups[\"value\"].Value\n            if (-not [string]::IsNullOrWhiteSpace($value)) {\n                return [ordered]@{\n                    key = $key\n                    value = $value\n                }\n            }\n        }\n    }\n\n    return $null\n}\n\nfunction Get-ConfigExtraction {\n    param(\n        [Parameter(Mandatory = $true)]\n        [string]$Path\n    )\n\n    $result = [ordered]@{\n        path = $Path\n        model = $null\n        reasoning = $null\n        provider = $null\n    }\n\n    if (-not (Test-Path -LiteralPath $Path)) {\n        Write-Verbose (\"Config path not found: {0}\" -f $Path)\n        return $result\n    }\n\n    Write-Verbose (\"Reading config candidate: {0}\" -f $Path)\n    $text = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop\n    if ([string]::IsNullOrWhiteSpace($text)) {\n        Write-Verbose (\"Config file was empty: {0}\" -f $Path)\n        return $result\n    }\n\n    $modelKeys = @(\n        \"model\",\n        \"default_model\",\n        \"model_name\",\n        \"engine\",\n        \"deployment\",\n        \"deployment_name\",\n        \"github.copilot.chat.model\"\n    )\n    $reasonKeys = @(\n        \"model_reasoning_effort\",\n        \"reasoning_level\",\n        \"reasoning_effort\",\n        \"reasoning\",\n        \"thinking\",\n        \"thinking_level\",\n        \"effort\"\n    )\n    $providerKeys = @(\n        \"provider\",\n        \"model_provider\",\n        \"vendor\"\n    )\n\n    $modelMatch = Get-KeyMatchFromText -Text $text -Keys $modelKeys\n    $reasonMatch = Get-KeyMatchFromText -Text $text -Keys $reasonKeys\n    $providerMatch = Get-KeyMatchFromText -Text $text -Keys $providerKeys\n\n    if ($null -ne $modelMatch) {\n        $result.model = $modelMatch\n        Write-Verbose (\"Model match in {0}: {1}={2}\" -f $Path, $modelMatch.key, $modelMatch.value)\n    }\n\n    if ($null -ne $reasonMatch) {\n        $result.reasoning = $reasonMatch\n        Write-Verbose (\"Reasoning match in {0}: {1}={2}\" -f $Path, $reasonMatch.key, $reasonMatch.value)\n    }\n\n    if ($null -ne $providerMatch) {\n        $result.provider = $providerMatch\n        Write-Verbose (\"Provider match in {0}: {1}={2}\" -f $Path, $providerMatch.key, $providerMatch.value)\n    }\n\n    return $result\n}\n\nfunction Parse-StatusMetadata {\n    param(\n        [AllowEmptyString()]\n        [string]$Text\n    )\n\n    $result = [ordered]@{\n        harness = $null\n        provider = $null\n        model = $null\n        reasoning = $null\n    }\n\n    if ([string]::IsNullOrWhiteSpace($Text)) {\n        return $result\n    }\n\n    Write-Verbose \"Parsing runtime status text.\"\n\n    $modelLine = [Regex]::Match($Text, \"(?im)^\\s*(model|active model)\\s*:\\s*(?<value>[^\\r\\n]+)\")\n    if ($modelLine.Success) {\n        $raw = $modelLine.Groups[\"value\"].Value.Trim()\n        $model = $raw\n        $reasoningFromParen = $null\n\n        $paren = [Regex]::Match($raw, \"^(?<model>[^(]+)\\((?<details>[^)]+)\\)\")\n        if ($paren.Success) {\n            $model = $paren.Groups[\"model\"].Value.Trim()\n            $details = $paren.Groups[\"details\"].Value\n            $reasoningMatch = [Regex]::Match($details, \"(?i)reasoning\\s+(?<reason>[^,\\)]+)\")\n            if ($reasoningMatch.Success) {\n                $reasoningFromParen = $reasoningMatch.Groups[\"reason\"].Value.Trim()\n            }\n        }\n\n        $result.model = [ordered]@{\n            key = \"Model\"\n            value = $model\n        }\n        if (-not [string]::IsNullOrWhiteSpace($reasoningFromParen)) {\n            $result.reasoning = [ordered]@{\n                key = \"Model(reasoning)\"\n                value = $reasoningFromParen\n            }\n        }\n    }\n\n    $reasoningLine = [Regex]::Match($Text, \"(?im)^\\s*(reasoning|effort|thinking)\\s*:\\s*(?<value>[^\\r\\n]+)\")\n    if ($reasoningLine.Success) {\n        $result.reasoning = [ordered]@{\n            key = $reasoningLine.Groups[1].Value\n            value = $reasoningLine.Groups[\"value\"].Value.Trim()\n        }\n    }\n\n    $harnessLine = [Regex]::Match($Text, \"(?im)^\\s*>\\s*_?\\s*(?<value>[^\\r\\n]+)\")\n    if ($harnessLine.Success) {\n        $header = $harnessLine.Groups[\"value\"].Value.Trim()\n        if ($header -match \"(?i)\\bcodex\\b\") {\n            $result.harness = [ordered]@{\n                key = \"header\"\n                value = \"codex\"\n            }\n        } elseif ($header -match \"(?i)\\bclaude\\b\") {\n            $result.harness = [ordered]@{\n                key = \"header\"\n                value = \"claude\"\n            }\n        } elseif ($header -match \"(?i)\\bcursor\\b\") {\n            $result.harness = [ordered]@{\n                key = \"header\"\n                value = \"cursor\"\n            }\n        } elseif ($header -match \"(?i)\\bcopilot\\b\") {\n            $result.harness = [ordered]@{\n                key = \"header\"\n                value = \"copilot\"\n            }\n        } elseif ($header -match \"(?i)\\bgemini\\b\") {\n            $result.harness = [ordered]@{\n                key = \"header\"\n                value = \"gemini\"\n            }\n        }\n    }\n\n    return $result\n}\n\n$fields = [ordered]@{\n    harness = New-FieldRecord -Name \"harness\"\n    provider = New-FieldRecord -Name \"provider\"\n    model = New-FieldRecord -Name \"model\"\n    reasoning_level = New-FieldRecord -Name \"reasoning_level\"\n}\n\n$checkedSources = New-Object System.Collections.Generic.List[string]\n\nWrite-Verbose (\"WorkspacePath: {0}\" -f $WorkspacePath)\nWrite-Verbose (\"OutputFormat: {0}\" -f $OutputFormat)\n\n$runtimeStatus = $null\nif (-not [string]::IsNullOrWhiteSpace($StatusFile)) {\n    if (Test-Path -LiteralPath $StatusFile) {\n        Write-Verbose (\"Loading status text from file: {0}\" -f $StatusFile)\n        $runtimeStatus = Get-Content -LiteralPath $StatusFile -Raw\n        $checkedSources.Add((\"status-file:{0}\" -f $StatusFile))\n    } else {\n        Write-Verbose (\"Status file not found: {0}\" -f $StatusFile)\n    }\n}\n\nif ([string]::IsNullOrWhiteSpace($runtimeStatus) -and -not [string]::IsNullOrWhiteSpace($StatusText)) {\n    Write-Verbose \"Using status text from -StatusText parameter.\"\n    $runtimeStatus = $StatusText\n    $checkedSources.Add(\"status-param\")\n}\n\nif (-not [string]::IsNullOrWhiteSpace($runtimeStatus)) {\n    $status = Parse-StatusMetadata -Text $runtimeStatus\n    if ($null -ne $status.harness) {\n        Set-FieldValue -Field $fields.harness -Value $status.harness.value -Source \"status\" -Evidence (\"status {0}\" -f $status.harness.key) -Confidence high\n    }\n    if ($null -ne $status.model) {\n        Set-FieldValue -Field $fields.model -Value $status.model.value -Source \"status\" -Evidence (\"status {0}\" -f $status.model.key) -Confidence high\n    }\n    if ($null -ne $status.reasoning) {\n        Set-FieldValue -Field $fields.reasoning_level -Value $status.reasoning.value -Source \"status\" -Evidence (\"status {0}\" -f $status.reasoning.key) -Confidence high\n    }\n}\n\n$ancestors = Get-ProcessAncestors\n$checkedSources.Add(\"process-tree\")\n\n$harnessFromProcess = Get-HarnessFromProcessTree -Ancestors $ancestors\nif ($null -ne $harnessFromProcess) {\n    Set-FieldValue -Field $fields.harness -Value $harnessFromProcess.harness -Source \"runtime-process\" -Evidence $harnessFromProcess.evidence -Confidence $harnessFromProcess.confidence\n}\n\n$harnessFromEnv = Get-HarnessFromEnvironment\n$checkedSources.Add(\"environment\")\nif ($null -ne $harnessFromEnv) {\n    Set-FieldValue -Field $fields.harness -Value $harnessFromEnv.harness -Source \"environment\" -Evidence $harnessFromEnv.evidence -Confidence $harnessFromEnv.confidence\n}\n\n$candidatePaths = Get-CandidateConfigPaths -WorkspaceRoot $WorkspacePath\n$checkedSources.Add(\"config-candidates\")\nWrite-Verbose (\"Evaluating {0} candidate config path(s).\" -f $candidatePaths.Count)\n\nforeach ($path in $candidatePaths) {\n    $extract = Get-ConfigExtraction -Path $path\n    if ($null -ne $extract.model) {\n        Set-FieldValue -Field $fields.model -Value $extract.model.value -Source \"config-file\" -Evidence (\"{0}:{1}\" -f $extract.path, $extract.model.key) -Confidence high\n    }\n\n    if ($null -ne $extract.reasoning) {\n        Set-FieldValue -Field $fields.reasoning_level -Value $extract.reasoning.value -Source \"config-file\" -Evidence (\"{0}:{1}\" -f $extract.path, $extract.reasoning.key) -Confidence high\n    }\n\n    if ($null -ne $extract.provider) {\n        Set-FieldValue -Field $fields.provider -Value $extract.provider.value -Source \"config-file\" -Evidence (\"{0}:{1}\" -f $extract.path, $extract.provider.key) -Confidence high\n    }\n}\n\n$modelEnvKeys = @(\"MODEL\", \"LLM_MODEL\", \"AI_MODEL\", \"OPENAI_MODEL\", \"ANTHROPIC_MODEL\", \"GEMINI_MODEL\", \"GOOGLE_MODEL\")\nforeach ($envKey in $modelEnvKeys) {\n    $envValue = [Environment]::GetEnvironmentVariable($envKey)\n    if (-not [string]::IsNullOrWhiteSpace($envValue)) {\n        Set-FieldValue -Field $fields.model -Value $envValue -Source \"environment\" -Evidence (\"env:{0}\" -f $envKey) -Confidence medium\n        break\n    }\n}\n\n$reasonEnvKeys = @(\"REASONING_LEVEL\", \"REASONING_EFFORT\", \"OPENAI_REASONING_EFFORT\", \"THINKING_LEVEL\", \"THINKING\")\nforeach ($envKey in $reasonEnvKeys) {\n    $envValue = [Environment]::GetEnvironmentVariable($envKey)\n    if (-not [string]::IsNullOrWhiteSpace($envValue)) {\n        Set-FieldValue -Field $fields.reasoning_level -Value $envValue -Source \"environment\" -Evidence (\"env:{0}\" -f $envKey) -Confidence medium\n        break\n    }\n}\n\nif ($fields.provider.value -eq \"unknown\") {\n    $providerFromModel = Get-ProviderFromModel -Model $fields.model.value\n    if ($providerFromModel -ne \"unknown\") {\n        Set-FieldValue -Field $fields.provider -Value $providerFromModel -Source \"inference-model\" -Evidence (\"model pattern match: {0}\" -f $fields.model.value) -Confidence medium\n    }\n}\n\nif ($fields.provider.value -eq \"unknown\") {\n    $providerFromHarness = Get-ProviderFromHarness -Harness $fields.harness.value\n    if ($providerFromHarness -ne \"unknown\") {\n        Set-FieldValue -Field $fields.provider -Value $providerFromHarness -Source \"inference-harness\" -Evidence (\"harness mapping: {0}\" -f $fields.harness.value) -Confidence low\n    }\n}\n\n$reasoningEquivalent = Convert-ToReasoningEquivalent -ReasoningLevel $fields.reasoning_level.value\n$highReasoning = $reasoningEquivalent -in @(\"high\", \"xhigh\")\n\n$sources = @(\n    @($fields.harness.source, $fields.provider.source, $fields.model.source, $fields.reasoning_level.source) |\n        Where-Object { $_ -ne \"unknown\" } |\n        Select-Object -Unique\n)\n\n$metadataSource =\n    if ($sources.Count -eq 0) {\n        \"unknown\"\n    } elseif ($sources.Count -eq 1) {\n        $sources[0]\n    } else {\n        \"mixed\"\n    }\n\n$result = [ordered]@{\n    harness = $fields.harness.value\n    provider = $fields.provider.value\n    model = $fields.model.value\n    reasoning_level = $fields.reasoning_level.value\n    reasoning_level_equivalent = $reasoningEquivalent\n    high_reasoning_asserted = [bool]$highReasoning\n    latest_model_asserted = $false\n    metadata_source = $metadataSource\n    metadata_evidence = [ordered]@{\n        harness = $fields.harness.evidence\n        provider = $fields.provider.evidence\n        model = $fields.model.evidence\n        reasoning_level = $fields.reasoning_level.evidence\n    }\n    confidence = [ordered]@{\n        harness = $fields.harness.confidence\n        provider = $fields.provider.confidence\n        model = $fields.model.confidence\n        reasoning_level = $fields.reasoning_level.confidence\n    }\n    checked_sources = @($checkedSources | Select-Object -Unique)\n    generated_at_utc = (Get-Date).ToUniversalTime().ToString(\"o\")\n}\n\n$output = $null\nswitch ($OutputFormat) {\n    \"Object\" {\n        $output = [pscustomobject]$result\n    }\n    \"Json\" {\n        $output = $result | ConvertTo-Json -Depth 6\n    }\n    \"Yaml\" {\n        $lines = New-Object System.Collections.Generic.List[string]\n        $lines.Add((\"harness: {0}\" -f $result.harness))\n        $lines.Add((\"provider: {0}\" -f $result.provider))\n        $lines.Add((\"model: {0}\" -f $result.model))\n        $lines.Add((\"reasoning_level: {0}\" -f $result.reasoning_level))\n        $lines.Add((\"reasoning_level_equivalent: {0}\" -f $result.reasoning_level_equivalent))\n        $lines.Add((\"high_reasoning_asserted: {0}\" -f $result.high_reasoning_asserted.ToString().ToLowerInvariant()))\n        $lines.Add((\"latest_model_asserted: {0}\" -f $result.latest_model_asserted.ToString().ToLowerInvariant()))\n        $lines.Add((\"metadata_source: {0}\" -f $result.metadata_source))\n        $lines.Add(\"metadata_evidence:\")\n        $lines.Add((\"  harness: {0}\" -f $result.metadata_evidence.harness))\n        $lines.Add((\"  provider: {0}\" -f $result.metadata_evidence.provider))\n        $lines.Add((\"  model: {0}\" -f $result.metadata_evidence.model))\n        $lines.Add((\"  reasoning_level: {0}\" -f $result.metadata_evidence.reasoning_level))\n        $lines.Add(\"confidence:\")\n        $lines.Add((\"  harness: {0}\" -f $result.confidence.harness))\n        $lines.Add((\"  provider: {0}\" -f $result.confidence.provider))\n        $lines.Add((\"  model: {0}\" -f $result.confidence.model))\n        $lines.Add((\"  reasoning_level: {0}\" -f $result.confidence.reasoning_level))\n        $lines.Add(\"checked_sources:\")\n        foreach ($source in $result.checked_sources) {\n            $lines.Add((\"  - {0}\" -f $source))\n        }\n        $lines.Add((\"generated_at_utc: {0}\" -f $result.generated_at_utc))\n        $output = $lines -join [Environment]::NewLine\n    }\n}\n\nif (-not [string]::IsNullOrWhiteSpace($OutputPath)) {\n    $outputDirectory = Split-Path -Path $OutputPath -Parent\n    if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) {\n        New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null\n    }\n\n    $serializedOutput =\n        if ($OutputFormat -eq \"Object\") {\n            $result | ConvertTo-Json -Depth 6\n        } else {\n            [string]$output\n        }\n\n    Set-Content -LiteralPath $OutputPath -Value $serializedOutput -Encoding utf8NoBOM\n    Write-Verbose (\"Wrote runtime metadata output to {0}\" -f $OutputPath)\n}\n\n$output\n"
  },
  {
    "path": "scripts/dev-local.ps1",
    "content": "#!/usr/bin/env pwsh\n<#\n  Compatibility wrapper for local onboarding.\n\n  Canonical script:\n    pwsh ./scripts/start-debuglocal.ps1\n\n  Run from repo root:\n    pwsh ./scripts/dev-local.ps1\n#>\n\n[CmdletBinding()]\nparam(\n  [string] $LaunchProfile = \"DebugLocal\",\n  [string] $GraceServerUri = \"http://localhost:5000\",\n  [string] $TestUserId = \"\",\n  [string] $BootstrapUserId = \"\",\n  [string] $TokenName = \"local-dev\",\n  [int]    $TokenDays = 30,\n  [int]    $StartupTimeoutSeconds = 240,\n  [switch] $NoBuild,\n  [switch] $NoTokenBootstrap\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path\n$startDebugLocalScript = Join-Path $scriptDir \"start-debuglocal.ps1\"\n\nif (-not (Test-Path $startDebugLocalScript)) {\n  throw \"Expected canonical script '$startDebugLocalScript' was not found.\"\n}\n\n$resolvedBootstrapUserId =\n  if (-not [string]::IsNullOrWhiteSpace($BootstrapUserId)) {\n    $BootstrapUserId.Trim()\n  } elseif (-not [string]::IsNullOrWhiteSpace($TestUserId)) {\n    $TestUserId.Trim()\n  } else {\n    \"\"\n  }\n\n$forwardedParameters = @{\n  LaunchProfile          = $LaunchProfile\n  GraceServerUri         = $GraceServerUri\n  TokenName              = $TokenName\n  TokenDays              = $TokenDays\n  StartupTimeoutSeconds  = $StartupTimeoutSeconds\n  NoBuild                = $NoBuild\n  NoTokenBootstrap       = $NoTokenBootstrap\n}\n\nif (-not [string]::IsNullOrWhiteSpace($resolvedBootstrapUserId)) {\n  $forwardedParameters[\"BootstrapUserId\"] = $resolvedBootstrapUserId\n}\n\nWrite-Host \"\"\nWrite-Host \"dev-local.ps1 is a compatibility alias.\" -ForegroundColor Yellow\nWrite-Host \"Using canonical script: ./scripts/start-debuglocal.ps1\" -ForegroundColor Yellow\nWrite-Host \"\"\n\n& $startDebugLocalScript @forwardedParameters\n"
  },
  {
    "path": "scripts/install-githooks.ps1",
    "content": "[CmdletBinding()]\nparam(\n    [switch]$Uninstall,\n    [switch]$Force\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$repoRoot = Resolve-Path (Join-Path $PSScriptRoot \"..\")\n$gitDir = Join-Path $repoRoot \".git\"\n\nif (-not (Test-Path $gitDir)) {\n    throw \"No .git directory found at $gitDir. Run from inside a Git clone.\"\n}\n\n$hookDir = Join-Path $gitDir \"hooks\"\n$hookPath = Join-Path $hookDir \"pre-commit\"\n$backupPath = Join-Path $hookDir \"pre-commit.grace.bak\"\n$marker = \"Grace Validate Hook\"\n\nfunction Get-FileContent([string]$Path) {\n    if (-not (Test-Path $Path)) {\n        return \"\"\n    }\n\n    return (Get-Content -Path $Path -Raw)\n}\n\nfunction Write-Hook([string]$Path) {\n    $hook = @\"\n#!/usr/bin/env sh\n# Grace Validate Hook (installed by scripts/install-githooks.ps1)\n# Runs validate -Fast after any existing hook logic.\n\nHOOK_DIR=$(cd \"$(dirname \"$0\")\" && pwd)\nBACKUP=\"$HOOK_DIR/pre-commit.grace.bak\"\n\nif [ -x \"$BACKUP\" ]; then\n  \"$BACKUP\" \"$@\" || exit $?\nfi\n\nif command -v pwsh >/dev/null 2>&1; then\n  REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)\n  if [ -n \"$REPO_ROOT\" ]; then\n    pwsh -NoProfile -ExecutionPolicy Bypass -File \"$REPO_ROOT/scripts/validate.ps1\" -Fast || exit $?\n  else\n    echo \"Grace hook: unable to locate repo root; skipping validate.\" >&2\n  fi\nelse\n  echo \"Grace hook: pwsh not found; skipping validate.\" >&2\nfi\n\"@\n\n    Set-Content -Path $Path -Value $hook -Encoding ASCII\n}\n\nif ($Uninstall) {\n    $current = Get-FileContent $hookPath\n\n    if ($current -match $marker) {\n        if (Test-Path $backupPath) {\n            Copy-Item -Path $backupPath -Destination $hookPath -Force\n            Remove-Item -Path $backupPath -Force\n            Write-Host \"Restored original pre-commit hook.\"\n        } else {\n            Remove-Item -Path $hookPath -Force\n            Write-Host \"Removed Grace pre-commit hook.\"\n        }\n    } else {\n        Write-Host \"Grace pre-commit hook not installed. No changes made.\"\n    }\n\n    return\n}\n\n$existing = Get-FileContent $hookPath\nif ($existing -match $marker) {\n    Write-Host \"Grace pre-commit hook already installed.\"\n    return\n}\n\nif (Test-Path $hookPath) {\n    if ((Test-Path $backupPath) -and -not $Force) {\n        throw \"Backup already exists at $backupPath. Use -Force to overwrite.\"\n    }\n\n    Copy-Item -Path $hookPath -Destination $backupPath -Force\n}\n\nWrite-Hook $hookPath\nWrite-Host \"Installed Grace pre-commit hook (validate -Fast).\"\nWrite-Host \"Run with -Uninstall to restore the previous hook.\"\r\n"
  },
  {
    "path": "scripts/start-debuglocal.ps1",
    "content": "#!/usr/bin/env pwsh\n<#\n  Canonical local onboarding script for Grace.\n\n  It starts Aspire with the DebugLocal launch profile, waits for Grace.Server\n  readiness, optionally probes auth readiness, bootstraps a PAT with TestAuth,\n  and prints copy-paste environment commands for PowerShell and bash/zsh shells.\n\n  Run from repo root:\n    pwsh ./scripts/start-debuglocal.ps1\n#>\n\n[CmdletBinding()]\nparam(\n  [string] $LaunchProfile = \"DebugLocal\",\n  [string] $ProjectPath = \"src/Grace.Aspire.AppHost/Grace.Aspire.AppHost.csproj\",\n  [string] $LaunchSettingsPath = \"src/Grace.Aspire.AppHost/Properties/launchSettings.json\",\n  [string] $GraceServerUri = \"http://localhost:5000\",\n  [string] $BootstrapUserId = \"\",\n  [string] $BootstrapUserIdFallback = \"test-admin\",\n  [string] $TokenName = \"local-dev\",\n  [int]    $TokenDays = 30,\n  [int]    $StartupTimeoutSeconds = 240,\n  [int]    $TokenBootstrapMaxAttempts = 4,\n  [int]    $TokenBootstrapInitialBackoffSeconds = 1,\n  [int]    $CleanupWaitSeconds = 5,\n  [switch] $NoBuild,\n  [switch] $NoTokenBootstrap,\n  [switch] $SkipAuthProbe\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$script:StepCounter = 0\n$script:LastFailureInfo = $null\n\nfunction Write-Step([string] $Message, [string] $Color = \"Cyan\") {\n  $script:StepCounter++\n  $timestamp = Get-Date -Format \"HH:mm:ss\"\n  Write-Host \"[$timestamp] Step $($script:StepCounter): $Message\" -ForegroundColor $Color\n}\n\nfunction Write-Detail([string] $Message, [string] $Color = \"DarkGray\") {\n  Write-Host \"  - $Message\" -ForegroundColor $Color\n}\n\nfunction Get-FirstUserId([string] $Users) {\n  if ([string]::IsNullOrWhiteSpace($Users)) {\n    return $null\n  }\n\n  $parts = $Users.Split(\";\", [System.StringSplitOptions]::RemoveEmptyEntries)\n  foreach ($part in $parts) {\n    $candidate = $part.Trim()\n    if (-not [string]::IsNullOrWhiteSpace($candidate)) {\n      return $candidate\n    }\n  }\n\n  return $null\n}\n\nfunction Merge-UserIds([string[]] $Values) {\n  $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)\n  $result = [System.Collections.Generic.List[string]]::new()\n\n  foreach ($value in $Values) {\n    if ([string]::IsNullOrWhiteSpace($value)) {\n      continue\n    }\n\n    $parts = $value.Split(\";\", [System.StringSplitOptions]::RemoveEmptyEntries)\n    foreach ($part in $parts) {\n      $candidate = $part.Trim()\n      if ([string]::IsNullOrWhiteSpace($candidate)) {\n        continue\n      }\n\n      if ($seen.Add($candidate)) {\n        $result.Add($candidate)\n      }\n    }\n  }\n\n  return [string]::Join(\";\", $result)\n}\n\nfunction Get-BootstrapUsersFromLaunchProfile([string] $LaunchSettingsFile, [string] $ProfileName) {\n  if ([string]::IsNullOrWhiteSpace($LaunchSettingsFile) -or -not (Test-Path $LaunchSettingsFile)) {\n    return $null\n  }\n\n  try {\n    $json = Get-Content -Raw -Path $LaunchSettingsFile | ConvertFrom-Json -Depth 20\n    if ($null -eq $json -or $null -eq $json.profiles) {\n      return $null\n    }\n\n    $profile = $json.profiles.$ProfileName\n    if ($null -eq $profile -or $null -eq $profile.environmentVariables) {\n      return $null\n    }\n\n    $value = [string] $profile.environmentVariables.grace__authz__bootstrap__system_admin_users\n    if ([string]::IsNullOrWhiteSpace($value)) {\n      return $null\n    }\n\n    return $value.Trim()\n  } catch {\n    Write-Detail \"Could not parse launch settings at '$LaunchSettingsFile': $($_.Exception.Message)\" \"Yellow\"\n    return $null\n  }\n}\n\nfunction Test-GraceHealth([string] $HealthUrl) {\n  try {\n    $response = Invoke-WebRequest -Uri $HealthUrl -Method GET -TimeoutSec 2\n    return ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300)\n  } catch {\n    return $false\n  }\n}\n\nfunction Wait-ForGraceServer(\n  [string] $HealthUrl,\n  [int] $TimeoutSeconds,\n  [int] $DelayMilliseconds,\n  [System.Diagnostics.Process] $AppHostProcess\n) {\n  $safeDelayMilliseconds = [Math]::Max($DelayMilliseconds, 250)\n  $maxAttempts = [Math]::Max([int][Math]::Ceiling(($TimeoutSeconds * 1000.0) / $safeDelayMilliseconds), 1)\n\n  for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {\n    if (Test-GraceHealth -HealthUrl $HealthUrl) {\n      return $true\n    }\n\n    if ($null -ne $AppHostProcess -and $AppHostProcess.HasExited) {\n      return $false\n    }\n\n    if ($attempt -eq 1 -or ($attempt % 10) -eq 0) {\n      Write-Detail \"Waiting for Grace.Server ($attempt/$maxAttempts): $HealthUrl\"\n    }\n\n    Start-Sleep -Milliseconds $safeDelayMilliseconds\n  }\n\n  return $false\n}\n\nfunction Resolve-PathFromRepoRoot([string] $RepoRoot, [string] $PathValue) {\n  if ([System.IO.Path]::IsPathRooted($PathValue)) {\n    return (Resolve-Path $PathValue).Path\n  }\n\n  return (Resolve-Path (Join-Path $RepoRoot $PathValue)).Path\n}\n\nfunction New-FailureInfo(\n  [string] $Stage,\n  [string] $Classification,\n  [Nullable[int]] $StatusCode,\n  [bool] $Retryable,\n  [string] $Message,\n  [string] $Hint\n) {\n  return [pscustomobject]@{\n    Stage          = $Stage\n    Classification = $Classification\n    StatusCode     = $StatusCode\n    Retryable      = $Retryable\n    Message        = $Message\n    Hint           = $Hint\n  }\n}\n\nfunction Set-LastFailureInfo([object] $FailureInfo) {\n  if ($null -ne $FailureInfo) {\n    $script:LastFailureInfo = $FailureInfo\n  }\n}\n\nfunction Get-HttpStatusCode([System.Management.Automation.ErrorRecord] $ErrorRecord) {\n  if ($null -eq $ErrorRecord -or $null -eq $ErrorRecord.Exception) {\n    return $null\n  }\n\n  $response = $null\n  if ($ErrorRecord.Exception.PSObject.Properties.Match(\"Response\").Count -gt 0) {\n    $response = $ErrorRecord.Exception.Response\n  }\n\n  if ($null -eq $response) {\n    return $null\n  }\n\n  if ($response.PSObject.Properties.Match(\"StatusCode\").Count -eq 0) {\n    return $null\n  }\n\n  try {\n    return [int]$response.StatusCode\n  } catch {\n    return $null\n  }\n}\n\nfunction Get-WebFailureInfo(\n  [System.Management.Automation.ErrorRecord] $ErrorRecord,\n  [string] $Stage,\n  [string] $GraceServerUri,\n  [string] $BootstrapUserId\n) {\n  $message = \"Unknown error.\"\n  if ($null -ne $ErrorRecord -and $null -ne $ErrorRecord.Exception -and -not [string]::IsNullOrWhiteSpace($ErrorRecord.Exception.Message)) {\n    $message = $ErrorRecord.Exception.Message.Trim()\n  }\n\n  $statusCode = Get-HttpStatusCode -ErrorRecord $ErrorRecord\n  $classification = \"unknown\"\n  $retryable = $false\n  $hint = \"Check AppHost logs and runtime metadata snapshot for details.\"\n\n  if ($null -ne $statusCode) {\n    if ($statusCode -in @(401, 403)) {\n      $classification = \"authorization\"\n      $retryable = $false\n      $hint =\n        \"Ensure '$BootstrapUserId' is in grace__authz__bootstrap__system_admin_users, and verify GRACE_TESTING=1 for local TestAuth.\"\n    } elseif ($statusCode -in @(400, 404)) {\n      $classification = \"configuration\"\n      $retryable = $false\n      $hint =\n        \"Verify DebugLocal configuration is active and auth endpoints are available at $GraceServerUri.\"\n    } elseif ($statusCode -in @(408, 425, 429) -or $statusCode -ge 500) {\n      $classification = \"transient\"\n      $retryable = $true\n      $hint =\n        \"The server may still be converging. The script will retry bounded transient failures.\"\n    }\n  } else {\n    if ($message -match \"(?i)timed out|timeout|refused|actively refused|connection reset|remote name could not be resolved|no such host|503\") {\n      $classification = \"transient\"\n      $retryable = $true\n      $hint =\n        \"The server appears unreachable or still starting. Verify startup logs and wait for health readiness.\"\n    } elseif ($message -match \"(?i)certificate|ssl|tls\") {\n      $classification = \"configuration\"\n      $retryable = $false\n      $hint =\n        \"TLS configuration failed for local endpoint. Verify URI and local certificate setup.\"\n    }\n  }\n\n  if ($Stage -eq \"auth probe\" -and $classification -ne \"transient\") {\n    $hint = \"$hint You can bypass this preflight with -SkipAuthProbe if intentionally testing without auth probe.\"\n  }\n\n  return New-FailureInfo `\n    -Stage $Stage `\n    -Classification $classification `\n    -StatusCode $statusCode `\n    -Retryable $retryable `\n    -Message $message `\n    -Hint $hint\n}\n\nfunction Get-RetryBackoffSeconds([int] $InitialBackoffSeconds, [int] $AttemptNumber) {\n  $safeInitialBackoff = [Math]::Max($InitialBackoffSeconds, 1)\n  $power = [Math]::Max($AttemptNumber - 1, 0)\n  $delay = [Math]::Min($safeInitialBackoff * [Math]::Pow(2, $power), 20)\n  return [int][Math]::Max([Math]::Round($delay), 1)\n}\n\nfunction Invoke-AuthProbe(\n  [string] $GraceServerUri,\n  [string] $BootstrapUserId\n) {\n  Write-Step \"Running auth readiness probe.\" \"Green\"\n\n  $oidcConfigUri = \"$GraceServerUri/auth/oidc/config\"\n  $authMeUri = \"$GraceServerUri/auth/me\"\n  $headers = @{ \"x-grace-user-id\" = $BootstrapUserId }\n\n  Write-Detail \"Probe 1/2: GET $oidcConfigUri\"\n  try {\n    $null = Invoke-WebRequest -Method Get -Uri $oidcConfigUri -TimeoutSec 10\n  } catch {\n    $failure = Get-WebFailureInfo -ErrorRecord $_ -Stage \"auth probe\" -GraceServerUri $GraceServerUri -BootstrapUserId $BootstrapUserId\n    Set-LastFailureInfo -FailureInfo $failure\n    $statusSegment = if ($null -eq $failure.StatusCode) { \"no-http-status\" } else { [string]$failure.StatusCode }\n    throw \"Auth probe failed at /auth/oidc/config (classification=$($failure.Classification), status=$statusSegment). $($failure.Hint)\"\n  }\n\n  Write-Detail \"Probe 2/2: GET $authMeUri (x-grace-user-id=$BootstrapUserId)\"\n  try {\n    $null = Invoke-WebRequest -Method Get -Uri $authMeUri -Headers $headers -TimeoutSec 10\n  } catch {\n    $failure = Get-WebFailureInfo -ErrorRecord $_ -Stage \"auth probe\" -GraceServerUri $GraceServerUri -BootstrapUserId $BootstrapUserId\n    Set-LastFailureInfo -FailureInfo $failure\n    $statusSegment = if ($null -eq $failure.StatusCode) { \"no-http-status\" } else { [string]$failure.StatusCode }\n    throw \"Auth probe failed at /auth/me (classification=$($failure.Classification), status=$statusSegment). $($failure.Hint)\"\n  }\n\n  Write-Detail \"Auth readiness probe passed.\" \"Green\"\n}\n\nfunction Invoke-TokenBootstrapWithRetry(\n  [string] $GraceServerUri,\n  [string] $BootstrapUserId,\n  [string] $RequestBodyJson,\n  [int] $MaxAttempts,\n  [int] $InitialBackoffSeconds\n) {\n  $tokenUri = \"$GraceServerUri/auth/token/create\"\n  $headers = @{ \"x-grace-user-id\" = $BootstrapUserId }\n  $safeMaxAttempts = [Math]::Max($MaxAttempts, 1)\n  $safeInitialBackoff = [Math]::Max($InitialBackoffSeconds, 1)\n  $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()\n\n  for ($attempt = 1; $attempt -le $safeMaxAttempts; $attempt++) {\n    Write-Detail \"Token bootstrap attempt $attempt/$safeMaxAttempts -> POST $tokenUri\"\n\n    try {\n      $response = Invoke-RestMethod `\n        -Method Post `\n        -Uri $tokenUri `\n        -Headers $headers `\n        -ContentType \"application/json\" `\n        -Body $RequestBodyJson `\n        -TimeoutSec 20\n\n      $stopwatch.Stop()\n      return [pscustomobject]@{\n        Response       = $response\n        Attempts       = $attempt\n        ElapsedSeconds = [Math]::Round($stopwatch.Elapsed.TotalSeconds, 2)\n      }\n    } catch {\n      $failure = Get-WebFailureInfo -ErrorRecord $_ -Stage \"token bootstrap\" -GraceServerUri $GraceServerUri -BootstrapUserId $BootstrapUserId\n      Set-LastFailureInfo -FailureInfo $failure\n\n      $statusSegment = if ($null -eq $failure.StatusCode) { \"no-http-status\" } else { [string]$failure.StatusCode }\n      Write-Detail \"Attempt $attempt failed (classification=$($failure.Classification), status=$statusSegment).\" \"Yellow\"\n\n      $isLastAttempt = $attempt -ge $safeMaxAttempts\n      if (-not $failure.Retryable -or $isLastAttempt) {\n        $stopwatch.Stop()\n        $retrySegment = if ($failure.Retryable) { \"retryable\" } else { \"non-retryable\" }\n        throw (\n          \"Token bootstrap failed after $attempt attempt(s) in $([Math]::Round($stopwatch.Elapsed.TotalSeconds, 2))s \" +\n          \"(classification=$($failure.Classification), status=$statusSegment, $retrySegment). $($failure.Hint)\"\n        )\n      }\n\n      $delaySeconds = Get-RetryBackoffSeconds -InitialBackoffSeconds $safeInitialBackoff -AttemptNumber $attempt\n      Write-Detail \"Transient failure detected. Retrying in $delaySeconds second(s).\" \"Yellow\"\n      Start-Sleep -Seconds $delaySeconds\n    }\n  }\n\n  throw \"Token bootstrap exhausted unexpectedly without a terminal response.\"\n}\n\nfunction Get-DebugLocalDotnetProcesses([string] $ProjectPath, [string] $LaunchProfile) {\n  $matches = [System.Collections.Generic.List[object]]::new()\n\n  $normalizedProjectPath = $ProjectPath.Replace(\"/\", \"\\\").ToLowerInvariant()\n  $normalizedLaunchProfile = $LaunchProfile.ToLowerInvariant()\n\n  $candidates = @()\n  try {\n    $candidates = Get-CimInstance Win32_Process -Filter \"Name='dotnet.exe' OR Name='dotnet'\" -ErrorAction Stop\n  } catch {\n    Write-Detail \"Unable to inspect existing dotnet processes: $($_.Exception.Message)\" \"Yellow\"\n    return @()\n  }\n\n  foreach ($candidate in $candidates) {\n    $commandLine = [string]$candidate.CommandLine\n    if ([string]::IsNullOrWhiteSpace($commandLine)) {\n      continue\n    }\n\n    $normalizedCommandLine = $commandLine.Replace(\"/\", \"\\\").ToLowerInvariant()\n    if (-not $normalizedCommandLine.Contains($normalizedProjectPath)) {\n      continue\n    }\n\n    if (-not $normalizedCommandLine.Contains(\"--launch-profile\")) {\n      continue\n    }\n\n    if (-not $normalizedCommandLine.Contains($normalizedLaunchProfile)) {\n      continue\n    }\n\n    $matches.Add([pscustomobject]@{\n      ProcessId   = [int]$candidate.ProcessId\n      Name        = [string]$candidate.Name\n      CommandLine = $commandLine\n    })\n  }\n\n  return @($matches)\n}\n\nfunction Stop-ProcessWithDiagnostics(\n  [int] $ProcessId,\n  [int] $WaitSeconds,\n  [string] $Context\n) {\n  $result = [ordered]@{\n    Context   = $Context\n    ProcessId = $ProcessId\n    Success   = $false\n    Message   = \"\"\n  }\n\n  $process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue\n  if ($null -eq $process) {\n    $result.Success = $true\n    $result.Message = \"Process $ProcessId is not running.\"\n    return [pscustomobject]$result\n  }\n\n  try {\n    Stop-Process -Id $ProcessId -ErrorAction Stop\n    Wait-Process -Id $ProcessId -Timeout ([Math]::Max($WaitSeconds, 1)) -ErrorAction SilentlyContinue\n  } catch {\n    Write-Detail \"Graceful stop attempt failed for PID ${ProcessId}: $($_.Exception.Message)\" \"Yellow\"\n  }\n\n  $stillRunning = $null -ne (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue)\n  if ($stillRunning) {\n    try {\n      Stop-Process -Id $ProcessId -Force -ErrorAction Stop\n      Wait-Process -Id $ProcessId -Timeout ([Math]::Max($WaitSeconds, 1)) -ErrorAction SilentlyContinue\n    } catch {\n      $result.Success = $false\n      $result.Message = \"Failed to stop process ${ProcessId}: $($_.Exception.Message)\"\n      return [pscustomobject]$result\n    }\n  }\n\n  $stillRunning = $null -ne (Get-Process -Id $ProcessId -ErrorAction SilentlyContinue)\n  if ($stillRunning) {\n    $result.Success = $false\n    $result.Message = \"Process $ProcessId is still running after force-stop attempt.\"\n  } else {\n    $result.Success = $true\n    $result.Message = \"Process $ProcessId stopped successfully.\"\n  }\n\n  return [pscustomobject]$result\n}\n\nfunction Write-LogTail([string] $LogPath, [int] $TailLines = 20) {\n  if ([string]::IsNullOrWhiteSpace($LogPath) -or -not (Test-Path $LogPath)) {\n    return\n  }\n\n  Write-Detail \"Recent log tail ($TailLines lines): $LogPath\" \"Yellow\"\n  Get-Content -Path $LogPath -Tail $TailLines | ForEach-Object {\n    Write-Host \"    $_\" -ForegroundColor DarkYellow\n  }\n}\n\nfunction Save-RuntimeMetadataSnapshot(\n  [string] $RepoRoot,\n  [string] $ScriptDirectory,\n  [string] $LogRoot,\n  [string] $RunId,\n  [System.Collections.Generic.List[string]] $DiagnosticsNotes\n) {\n  $metadataScriptPath = Join-Path $ScriptDirectory \"collect-runtime-metadata.ps1\"\n  if (-not (Test-Path $metadataScriptPath)) {\n    $DiagnosticsNotes.Add(\"Runtime metadata script was not found at '$metadataScriptPath'.\")\n    return $null\n  }\n\n  $metadataPath = Join-Path $LogRoot \"start-debuglocal-$RunId.runtime-metadata.json\"\n  try {\n    & $metadataScriptPath -WorkspacePath $RepoRoot -OutputFormat Json -OutputPath $metadataPath | Out-Null\n    $DiagnosticsNotes.Add(\"Runtime metadata snapshot written to '$metadataPath'.\")\n    return $metadataPath\n  } catch {\n    $DiagnosticsNotes.Add(\"Failed to write runtime metadata snapshot: $($_.Exception.Message)\")\n    return $null\n  }\n}\n\nfunction Write-FailureDiagnostics(\n  [string] $RepoRoot,\n  [string] $ScriptDirectory,\n  [string] $LogRoot,\n  [string] $RunId,\n  [string] $GraceServerUri,\n  [string] $HealthUrl,\n  [string] $BootstrapUserId,\n  [string] $StdoutLog,\n  [string] $StderrLog,\n  [System.Collections.Generic.List[string]] $CleanupNotes,\n  [System.Management.Automation.ErrorRecord] $ErrorRecord\n) {\n  Write-Step \"Collecting failure diagnostics.\" \"Red\"\n\n  if ($null -eq $script:LastFailureInfo) {\n    $fallbackMessage =\n      if ($null -ne $ErrorRecord -and $null -ne $ErrorRecord.Exception) {\n        $ErrorRecord.Exception.Message\n      } else {\n        \"Unknown failure\"\n      }\n\n    $script:LastFailureInfo = New-FailureInfo `\n      -Stage \"startup workflow\" `\n      -Classification \"unknown\" `\n      -StatusCode $null `\n      -Retryable $false `\n      -Message $fallbackMessage `\n      -Hint \"Inspect logs and runtime metadata for root cause.\"\n  }\n\n  Write-Detail \"Failure stage: $($script:LastFailureInfo.Stage)\" \"Red\"\n  Write-Detail \"Failure classification: $($script:LastFailureInfo.Classification)\" \"Red\"\n  Write-Detail \"Failure message: $($script:LastFailureInfo.Message)\" \"Red\"\n  if ($null -ne $script:LastFailureInfo.StatusCode) {\n    Write-Detail \"HTTP status: $($script:LastFailureInfo.StatusCode)\" \"Red\"\n  }\n  Write-Detail \"Suggested next step: $($script:LastFailureInfo.Hint)\" \"Yellow\"\n\n  $runtimeMetadataPath = Save-RuntimeMetadataSnapshot `\n    -RepoRoot $RepoRoot `\n    -ScriptDirectory $ScriptDirectory `\n    -LogRoot $LogRoot `\n    -RunId $RunId `\n    -DiagnosticsNotes $CleanupNotes\n\n  if (-not [string]::IsNullOrWhiteSpace($runtimeMetadataPath)) {\n    Write-Detail \"Runtime metadata: $runtimeMetadataPath\" \"Yellow\"\n  }\n\n  if (-not [string]::IsNullOrWhiteSpace($StdoutLog)) {\n    Write-Detail \"AppHost stdout log: $StdoutLog\" \"Yellow\"\n    Write-LogTail -LogPath $StdoutLog -TailLines 20\n  }\n\n  if (-not [string]::IsNullOrWhiteSpace($StderrLog)) {\n    Write-Detail \"AppHost stderr log: $StderrLog\" \"Yellow\"\n    Write-LogTail -LogPath $StderrLog -TailLines 20\n  }\n\n  $failureSummary = [ordered]@{\n    generated_at_utc = (Get-Date).ToUniversalTime().ToString(\"o\")\n    grace_server_uri = $GraceServerUri\n    health_url = $HealthUrl\n    bootstrap_user_id = $BootstrapUserId\n    stage = $script:LastFailureInfo.Stage\n    classification = $script:LastFailureInfo.Classification\n    status_code = $script:LastFailureInfo.StatusCode\n    message = $script:LastFailureInfo.Message\n    hint = $script:LastFailureInfo.Hint\n    stdout_log = $StdoutLog\n    stderr_log = $StderrLog\n    runtime_metadata = $runtimeMetadataPath\n    cleanup_notes = @($CleanupNotes)\n  }\n\n  $summaryPath = Join-Path $LogRoot \"start-debuglocal-$RunId.failure.json\"\n  try {\n    $failureSummary | ConvertTo-Json -Depth 6 | Set-Content -Path $summaryPath -Encoding utf8NoBOM\n    Write-Detail \"Failure summary JSON: $summaryPath\" \"Yellow\"\n  } catch {\n    Write-Detail \"Failed to write failure summary JSON: $($_.Exception.Message)\" \"Yellow\"\n  }\n}\n\n$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path\n$repoRoot = (Resolve-Path (Join-Path $scriptDir \"..\")).Path\nSet-Location $repoRoot\n\n$logRoot = Join-Path $repoRoot \".grace/logs\"\nNew-Item -ItemType Directory -Path $logRoot -Force | Out-Null\n\n$runId = Get-Date -Format \"yyyyMMdd-HHmmss\"\n$resolvedProjectPath = \"\"\n$resolvedLaunchSettingsPath = \"\"\n$healthUrl = \"$GraceServerUri/healthz\"\n$appHostProcess = $null\n$stdoutLog = \"\"\n$stderrLog = \"\"\n$resolvedBootstrapUserId = \"\"\n$resolvedBootstrapSource = \"\"\n$cleanupNotes = [System.Collections.Generic.List[string]]::new()\n$startedAppHostThisRun = $false\n$capturedError = $null\n$failed = $false\n\ntry {\n  $resolvedProjectPath = Resolve-PathFromRepoRoot -RepoRoot $repoRoot -PathValue $ProjectPath\n  $resolvedLaunchSettingsPath = Resolve-PathFromRepoRoot -RepoRoot $repoRoot -PathValue $LaunchSettingsPath\n  $healthUrl = \"$GraceServerUri/healthz\"\n\n  Write-Step \"Preparing DebugLocal startup context.\"\n  Write-Detail \"Canonical script: pwsh ./scripts/start-debuglocal.ps1\"\n  Write-Detail \"Compatibility alias: pwsh ./scripts/dev-local.ps1\"\n  Write-Detail \"Repo root: $repoRoot\"\n  Write-Detail \"Project path: $resolvedProjectPath\"\n  Write-Detail \"Launch settings path: $resolvedLaunchSettingsPath\"\n  Write-Detail \"Launch profile: $LaunchProfile\"\n  Write-Detail \"Grace server URI: $GraceServerUri\"\n  Write-Detail \"Startup timeout (seconds): $StartupTimeoutSeconds\"\n  Write-Detail \"Token max attempts: $TokenBootstrapMaxAttempts\"\n  Write-Detail \"Token initial backoff (seconds): $TokenBootstrapInitialBackoffSeconds\"\n  Write-Detail \"NoBuild: $NoBuild\"\n  Write-Detail \"NoTokenBootstrap: $NoTokenBootstrap\"\n  Write-Detail \"SkipAuthProbe: $SkipAuthProbe\"\n\n  Write-Step \"Resolving bootstrap user identity with deterministic precedence.\"\n\n  $trimmedParameterBootstrapUserId =\n    if ([string]::IsNullOrWhiteSpace($BootstrapUserId)) { \"\" } else { $BootstrapUserId.Trim() }\n\n  if (-not [string]::IsNullOrWhiteSpace($trimmedParameterBootstrapUserId)) {\n    $resolvedBootstrapUserId = $trimmedParameterBootstrapUserId\n    $resolvedBootstrapSource = \"script parameter (-BootstrapUserId)\"\n  }\n\n  $existingBootstrapUsers = $env:grace__authz__bootstrap__system_admin_users\n  $firstEnvBootstrapUser = Get-FirstUserId $existingBootstrapUsers\n  if ([string]::IsNullOrWhiteSpace($resolvedBootstrapUserId) -and -not [string]::IsNullOrWhiteSpace($firstEnvBootstrapUser)) {\n    $resolvedBootstrapUserId = $firstEnvBootstrapUser\n    $resolvedBootstrapSource = \"environment variable (grace__authz__bootstrap__system_admin_users)\"\n  }\n\n  $launchProfileBootstrapUsers = Get-BootstrapUsersFromLaunchProfile -LaunchSettingsFile $resolvedLaunchSettingsPath -ProfileName $LaunchProfile\n  $firstLaunchProfileBootstrapUser = Get-FirstUserId $launchProfileBootstrapUsers\n  if ([string]::IsNullOrWhiteSpace($resolvedBootstrapUserId) -and -not [string]::IsNullOrWhiteSpace($firstLaunchProfileBootstrapUser)) {\n    $resolvedBootstrapUserId = $firstLaunchProfileBootstrapUser\n    $resolvedBootstrapSource = \"launch profile value ($LaunchProfile in launchSettings.json)\"\n  }\n\n  $trimmedFallbackBootstrapUserId =\n    if ([string]::IsNullOrWhiteSpace($BootstrapUserIdFallback)) { \"\" } else { $BootstrapUserIdFallback.Trim() }\n\n  if ([string]::IsNullOrWhiteSpace($resolvedBootstrapUserId) -and -not [string]::IsNullOrWhiteSpace($trimmedFallbackBootstrapUserId)) {\n    $resolvedBootstrapUserId = $trimmedFallbackBootstrapUserId\n    $resolvedBootstrapSource = \"fallback default (-BootstrapUserIdFallback)\"\n  }\n\n  if ([string]::IsNullOrWhiteSpace($resolvedBootstrapUserId)) {\n    throw \"Could not resolve a bootstrap user ID. Pass -BootstrapUserId explicitly.\"\n  }\n\n  $effectiveBootstrapUsers = Merge-UserIds @($existingBootstrapUsers, $resolvedBootstrapUserId)\n  if ([string]::IsNullOrWhiteSpace($effectiveBootstrapUsers)) {\n    throw \"Could not compute effective bootstrap users.\"\n  }\n\n  $env:grace__authz__bootstrap__system_admin_users = $effectiveBootstrapUsers\n\n  $graceTestingValue = $env:GRACE_TESTING\n  $graceTestingSource =\n    if ([string]::IsNullOrWhiteSpace($graceTestingValue)) {\n      $env:GRACE_TESTING = \"1\"\n      \"script defaulted to 1 for TestAuth\"\n    } else {\n      \"inherited from shell\"\n    }\n\n  Write-Detail \"Resolved bootstrap user ID: $resolvedBootstrapUserId\" \"Green\"\n  Write-Detail \"Resolved bootstrap source: $resolvedBootstrapSource\" \"Green\"\n  Write-Detail \"Effective bootstrap users env value: $effectiveBootstrapUsers\" \"Green\"\n  Write-Detail \"Launch profile bootstrap users value: $(if ([string]::IsNullOrWhiteSpace($launchProfileBootstrapUsers)) { '<none>' } else { $launchProfileBootstrapUsers })\"\n  Write-Detail \"GRACE_TESTING: $($env:GRACE_TESTING) ($graceTestingSource)\"\n\n  $serverAlreadyRunning = Test-GraceHealth -HealthUrl $healthUrl\n  if ($serverAlreadyRunning) {\n    Write-Step \"Grace.Server is already healthy; skipping AppHost startup.\" \"Green\"\n  } else {\n    $staleProcesses = Get-DebugLocalDotnetProcesses -ProjectPath $resolvedProjectPath -LaunchProfile $LaunchProfile\n    if ($staleProcesses.Count -gt 0) {\n      Write-Step \"Detected $($staleProcesses.Count) stale DebugLocal AppHost process(es); cleaning up before startup.\" \"Yellow\"\n      foreach ($staleProcess in $staleProcesses) {\n        Write-Detail \"Stale process PID $($staleProcess.ProcessId): $($staleProcess.CommandLine)\" \"Yellow\"\n        $cleanupResult = Stop-ProcessWithDiagnostics -ProcessId $staleProcess.ProcessId -WaitSeconds $CleanupWaitSeconds -Context \"stale-process-cleanup\"\n        $note = \"stale-process-cleanup pid=$($cleanupResult.ProcessId) success=$($cleanupResult.Success) message='$($cleanupResult.Message)'\"\n        $cleanupNotes.Add($note)\n        Write-Detail $note ($(if ($cleanupResult.Success) { \"Green\" } else { \"Red\" }))\n      }\n    }\n\n    Write-Step \"Starting Grace.Aspire.AppHost in the background.\" \"Green\"\n\n    $stdoutLog = Join-Path $logRoot \"start-debuglocal-$runId.stdout.log\"\n    $stderrLog = Join-Path $logRoot \"start-debuglocal-$runId.stderr.log\"\n\n    $dotnetArgs = @(\"run\", \"--project\", $resolvedProjectPath, \"--launch-profile\", $LaunchProfile)\n    if ($NoBuild) {\n      $dotnetArgs += \"--no-build\"\n    }\n\n    Write-Detail \"AppHost stdout log: $stdoutLog\"\n    Write-Detail \"AppHost stderr log: $stderrLog\"\n    Write-Detail \"Command: dotnet $($dotnetArgs -join ' ')\"\n\n    $appHostProcess = Start-Process `\n      -FilePath \"dotnet\" `\n      -ArgumentList $dotnetArgs `\n      -WorkingDirectory $repoRoot `\n      -RedirectStandardOutput $stdoutLog `\n      -RedirectStandardError $stderrLog `\n      -PassThru\n\n    $startedAppHostThisRun = $true\n    Write-Detail \"AppHost PID: $($appHostProcess.Id)\"\n\n    Write-Step \"Waiting for Grace.Server health endpoint.\"\n    $healthy = Wait-ForGraceServer -HealthUrl $healthUrl -TimeoutSeconds $StartupTimeoutSeconds -DelayMilliseconds 1000 -AppHostProcess $appHostProcess\n\n    if (-not $healthy) {\n      if ($null -ne $appHostProcess -and $appHostProcess.HasExited) {\n        $message = \"AppHost exited early with code $($appHostProcess.ExitCode).\"\n        Write-Detail $message \"Red\"\n        Set-LastFailureInfo -FailureInfo (New-FailureInfo -Stage \"server startup\" -Classification \"configuration\" -StatusCode $null -Retryable $false -Message $message -Hint \"Inspect AppHost logs for startup errors and launch profile configuration.\")\n      } else {\n        $message = \"Grace.Server health check timed out after $StartupTimeoutSeconds second(s).\"\n        Write-Detail $message \"Red\"\n        Set-LastFailureInfo -FailureInfo (New-FailureInfo -Stage \"server startup\" -Classification \"transient\" -StatusCode $null -Retryable $true -Message $message -Hint \"The runtime did not reach healthy state in time. Check AppHost logs and consider increasing -StartupTimeoutSeconds.\")\n      }\n\n      throw \"Grace.Server did not become healthy at $healthUrl.\"\n    }\n\n    Write-Detail \"Grace.Server is healthy.\" \"Green\"\n  }\n\n  if (-not $SkipAuthProbe) {\n    Invoke-AuthProbe -GraceServerUri $GraceServerUri -BootstrapUserId $resolvedBootstrapUserId\n  } else {\n    Write-Step \"Auth readiness probe skipped by request (-SkipAuthProbe).\" \"Yellow\"\n  }\n\n  if (-not $NoTokenBootstrap) {\n    Write-Step \"Creating a personal access token using TestAuth context.\" \"Green\"\n\n    $requestedTokenName =\n      if ([string]::IsNullOrWhiteSpace($TokenName)) {\n        \"local-dev\"\n      } else {\n        $TokenName.Trim()\n      }\n\n    $expiresInSeconds = [int64]($TokenDays * 86400L)\n    $requestBody = @{\n      TokenName        = $requestedTokenName\n      ExpiresInSeconds = $expiresInSeconds\n      NoExpiry         = $false\n    } | ConvertTo-Json\n\n    Write-Detail \"Token user ID: $resolvedBootstrapUserId\"\n    Write-Detail \"Token name: $requestedTokenName\"\n    Write-Detail \"Token lifetime (days): $TokenDays\"\n\n    $tokenBootstrapResult = Invoke-TokenBootstrapWithRetry `\n      -GraceServerUri $GraceServerUri `\n      -BootstrapUserId $resolvedBootstrapUserId `\n      -RequestBodyJson $requestBody `\n      -MaxAttempts $TokenBootstrapMaxAttempts `\n      -InitialBackoffSeconds $TokenBootstrapInitialBackoffSeconds\n\n    Write-Detail \"Token bootstrap completed in $($tokenBootstrapResult.Attempts) attempt(s), $($tokenBootstrapResult.ElapsedSeconds)s total.\" \"Green\"\n\n    $response = $tokenBootstrapResult.Response\n    $token = $null\n    if ($null -ne $response.ReturnValue -and $null -ne $response.ReturnValue.Token) {\n      $token = [string]$response.ReturnValue.Token\n    } elseif ($null -ne $response.Token) {\n      $token = [string]$response.Token\n    }\n\n    if ([string]::IsNullOrWhiteSpace($token)) {\n      Set-LastFailureInfo -FailureInfo (\n        New-FailureInfo `\n          -Stage \"token bootstrap\" `\n          -Classification \"unknown\" `\n          -StatusCode $null `\n          -Retryable $false `\n          -Message \"Token endpoint returned success, but token value was empty.\" `\n          -Hint \"Verify auth response shape and inspect server logs for serialization or contract errors.\"\n      )\n      throw \"Token creation succeeded but no token was returned.\"\n    }\n\n    $env:GRACE_SERVER_URI = $GraceServerUri\n    $env:GRACE_TOKEN = $token\n\n    Write-Detail \"Set GRACE_SERVER_URI and GRACE_TOKEN for this shell.\" \"Green\"\n\n    Write-Host \"\"\n    Write-Host \"Copy/paste (PowerShell):\" -ForegroundColor Cyan\n    Write-Host \"  `$env:GRACE_SERVER_URI = `\"$GraceServerUri`\"\"\n    Write-Host \"  `$env:GRACE_TOKEN      = `\"$token`\"\"\n    Write-Host \"\"\n    Write-Host \"Copy/paste (bash/zsh):\" -ForegroundColor Cyan\n    Write-Host \"  export GRACE_SERVER_URI=`\"$GraceServerUri`\"\"\n    Write-Host \"  export GRACE_TOKEN=`\"$token`\"\"\n    Write-Host \"\"\n  } else {\n    Write-Step \"Token bootstrap skipped by request (-NoTokenBootstrap).\" \"Yellow\"\n  }\n\n  Write-Step \"DebugLocal startup workflow is complete.\" \"Green\"\n  Write-Detail \"Bootstrap user ID: $resolvedBootstrapUserId\"\n  Write-Detail \"Bootstrap source: $resolvedBootstrapSource\"\n  Write-Detail \"Grace server URI: $GraceServerUri\"\n\n  if ($null -ne $appHostProcess) {\n    Write-Detail \"AppHost PID: $($appHostProcess.Id)\"\n    Write-Detail \"Stop command: Stop-Process -Id $($appHostProcess.Id)\" \"Yellow\"\n  }\n\n  if (-not [string]::IsNullOrWhiteSpace($stdoutLog)) {\n    Write-Detail \"AppHost stdout log: $stdoutLog\"\n  }\n\n  if (-not [string]::IsNullOrWhiteSpace($stderrLog)) {\n    Write-Detail \"AppHost stderr log: $stderrLog\"\n  }\n} catch {\n  $capturedError = $_\n  $failed = $true\n} finally {\n  if ($failed) {\n    if ($null -ne $appHostProcess -and $startedAppHostThisRun) {\n      Write-Step \"Running failure-path cleanup for AppHost process.\" \"Yellow\"\n      $cleanupResult = Stop-ProcessWithDiagnostics -ProcessId $appHostProcess.Id -WaitSeconds $CleanupWaitSeconds -Context \"failure-cleanup\"\n      $note = \"failure-cleanup pid=$($cleanupResult.ProcessId) success=$($cleanupResult.Success) message='$($cleanupResult.Message)'\"\n      $cleanupNotes.Add($note)\n      Write-Detail $note ($(if ($cleanupResult.Success) { \"Green\" } else { \"Red\" }))\n    } elseif ($null -ne $appHostProcess) {\n      $note = \"failure-cleanup skipped for pid=$($appHostProcess.Id) because process was not started by this run.\"\n      $cleanupNotes.Add($note)\n      Write-Detail $note \"Yellow\"\n    } else {\n      $note = \"failure-cleanup skipped because no AppHost process was created.\"\n      $cleanupNotes.Add($note)\n      Write-Detail $note \"Yellow\"\n    }\n\n    Write-FailureDiagnostics `\n      -RepoRoot $repoRoot `\n      -ScriptDirectory $scriptDir `\n      -LogRoot $logRoot `\n      -RunId $runId `\n      -GraceServerUri $GraceServerUri `\n      -HealthUrl $healthUrl `\n      -BootstrapUserId $resolvedBootstrapUserId `\n      -StdoutLog $stdoutLog `\n      -StderrLog $stderrLog `\n      -CleanupNotes $cleanupNotes `\n      -ErrorRecord $capturedError\n\n    if ($null -ne $capturedError) {\n      throw $capturedError\n    }\n\n    throw \"DebugLocal startup failed.\"\n  }\n}\n"
  },
  {
    "path": "scripts/validate.ps1",
    "content": "[CmdletBinding()]\nparam(\n    [switch]$Fast,\n    [switch]$Full,\n    [switch]$SkipFormat,\n    [switch]$SkipBuild,\n    [switch]$SkipTests,\n    [string]$Configuration = \"Release\"\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$startTime = Get-Date\n$exitCode = 0\n$formatDisabled = $true\n\nfunction Write-Section([string]$Title) {\n    Write-Host \"\"\n    Write-Host (\"== {0} ==\" -f $Title)\n}\n\nfunction Get-FormatTargets {\n    $targets = @()\n    $git = Get-Command git -ErrorAction SilentlyContinue\n    $separator = [System.IO.Path]::DirectorySeparatorChar\n    $prefix = \"src{0}\" -f $separator\n    $isCi = $env:GITHUB_ACTIONS -eq \"true\" -or $env:CI -eq \"true\"\n\n    if ($isCi -and $null -ne $git) {\n        $diffPaths = @()\n        $event = $null\n\n        if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_EVENT_PATH) -and (Test-Path $env:GITHUB_EVENT_PATH)) {\n            try {\n                $event = Get-Content $env:GITHUB_EVENT_PATH -Raw | ConvertFrom-Json\n            } catch {\n                $event = $null\n            }\n        }\n\n        if ($env:GITHUB_EVENT_NAME -like \"pull_request*\") {\n            $baseSha =\n                if ($null -ne $event -and $null -ne $event.pull_request) { $event.pull_request.base.sha } else { $null }\n\n            if ([string]::IsNullOrWhiteSpace($baseSha) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_BASE_SHA)) {\n                $baseSha = $env:GITHUB_BASE_SHA\n            }\n\n            if (-not [string]::IsNullOrWhiteSpace($baseSha)) {\n                $null = & git fetch --no-tags --depth=1 origin $baseSha 2>$null\n                $diffPaths = & git diff --name-only $baseSha HEAD 2>$null\n                if ($LASTEXITCODE -ne 0) {\n                    $diffPaths = @()\n                }\n            }\n        } elseif ($env:GITHUB_EVENT_NAME -eq \"push\") {\n            $baseSha = if ($null -ne $event) { $event.before } else { $null }\n            $headSha = if ($null -ne $event) { $event.after } else { $null }\n\n            if ([string]::IsNullOrWhiteSpace($baseSha) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_EVENT_BEFORE)) {\n                $baseSha = $env:GITHUB_EVENT_BEFORE\n            }\n\n            if ([string]::IsNullOrWhiteSpace($headSha) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_SHA)) {\n                $headSha = $env:GITHUB_SHA\n            }\n\n            if (-not [string]::IsNullOrWhiteSpace($baseSha) -and -not [string]::IsNullOrWhiteSpace($headSha)) {\n                $null = & git fetch --no-tags --depth=1 origin $baseSha $headSha 2>$null\n                $diffPaths = & git diff --name-only $baseSha $headSha 2>$null\n                if ($LASTEXITCODE -ne 0) {\n                    $diffPaths = @()\n                }\n            }\n        }\n\n        if (-not $diffPaths -or $diffPaths.Count -eq 0) {\n            $diffPaths = & git diff --name-only HEAD~1 HEAD 2>$null\n            if ($LASTEXITCODE -ne 0) {\n                $diffPaths = @()\n            }\n        }\n\n        foreach ($path in $diffPaths) {\n            if ([string]::IsNullOrWhiteSpace($path)) {\n                continue\n            }\n\n            $path = $path -replace \"[\\\\/]\", $separator\n            if (-not $path.StartsWith($prefix)) {\n                continue\n            }\n\n            $extension = [System.IO.Path]::GetExtension($path).ToLowerInvariant()\n            if ($extension -in @(\".fs\", \".fsi\", \".fsx\")) {\n                $targets += $path\n            }\n        }\n\n        return [string[]]($targets | Select-Object -Unique)\n    }\n\n    if ($null -ne $git) {\n        $statusLines = & git status --porcelain\n        foreach ($line in $statusLines) {\n            if ([string]::IsNullOrWhiteSpace($line)) {\n                continue\n            }\n\n            $path = $line.Substring(3)\n            if ($path -match \" -> \") {\n                $path = $path.Split(\" -> \")[-1]\n            }\n\n            $path = $path -replace \"[\\\\/]\", $separator\n            if (-not $path.StartsWith($prefix)) {\n                continue\n            }\n\n            $extension = [System.IO.Path]::GetExtension($path).ToLowerInvariant()\n            if ($extension -in @(\".fs\", \".fsi\", \".fsx\")) {\n                $targets += $path\n            }\n        }\n    }\n\n    [string[]]($targets | Select-Object -Unique)\n}\n\nfunction Invoke-External([string]$Label, [scriptblock]$Command) {\n    & $Command\n    if ($LASTEXITCODE -ne 0) {\n        throw \"$Label failed with exit code $LASTEXITCODE.\"\n    }\n}\n\ntry {\n    if (-not $Fast -and -not $Full) {\n        $Fast = $true\n    }\n\n    if ($Fast -and $Full) {\n        throw \"Choose either -Fast or -Full, not both.\"\n    }\n\n    if ($formatDisabled) {\n        Write-Section \"Format\"\n        Write-Host \"Skipped (temporarily disabled pending full repo formatting).\"\n    } elseif (-not $SkipFormat) {\n        Write-Section \"Format\"\n        Invoke-External \"dotnet tool restore\" { dotnet tool restore }\n\n        $formatTargets = Get-FormatTargets\n        if (-not $formatTargets -or $formatTargets.Length -eq 0) {\n            Write-Host \"No changed F# files detected. Skipping format check.\"\n        } else {\n            $separator = [System.IO.Path]::DirectorySeparatorChar\n            $prefix = \"src{0}\" -f $separator\n            $relativeTargets =\n                $formatTargets\n                | ForEach-Object { $_.Substring($prefix.Length) }\n\n            Push-Location \"src\"\n            try {\n                & dotnet tool run fantomas --check @relativeTargets\n                if ($LASTEXITCODE -ne 0) {\n                    if ($LASTEXITCODE -eq 1) {\n                        throw \"Formatting drift detected. Run 'dotnet tool run fantomas --recurse .' from ./src to apply formatting.\"\n                    }\n\n                    throw \"Fantomas failed with exit code $LASTEXITCODE.\"\n                }\n            } finally {\n                Pop-Location\n            }\n        }\n    } else {\n        Write-Section \"Format\"\n        Write-Host \"Skipped (-SkipFormat).\"\n    }\n\n    if (-not $SkipBuild) {\n        Write-Section \"Build\"\n        Invoke-External \"Grace solution build\" { dotnet build \"src/Grace.slnx\" -c $Configuration }\n    } else {\n        Write-Section \"Build\"\n        Write-Host \"Skipped (-SkipBuild).\"\n    }\n\n    if (-not $SkipTests) {\n        Write-Section \"Test\"\n        Invoke-External \"Grace.Authorization.Tests\" { dotnet test \"src/Grace.Authorization.Tests/Grace.Authorization.Tests.fsproj\" -c $Configuration --no-build }\n        Invoke-External \"Grace.CLI.Tests\" { dotnet test \"src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj\" -c $Configuration --no-build }\n        Invoke-External \"Grace.Types.Tests\" { dotnet test \"src/Grace.Types.Tests/Grace.Types.Tests.fsproj\" -c $Configuration --no-build }\n\n        if ($Full) {\n            Invoke-External \"Grace.Server.Tests\" { dotnet test \"src/Grace.Server.Tests/Grace.Server.Tests.fsproj\" -c $Configuration --no-build }\n        }\n    } else {\n        Write-Section \"Test\"\n        Write-Host \"Skipped (-SkipTests).\"\n    }\n} catch {\n    $exitCode = 1\n    $message =\n        if ($null -ne $_.Exception -and -not [string]::IsNullOrWhiteSpace($_.Exception.Message)) {\n            $_.Exception.Message\n        } else {\n            $_.ToString()\n        }\n\n    Write-Error (\"Validation failed: {0}\" -f $message)\n} finally {\n    $elapsed = (Get-Date) - $startTime\n    Write-Host \"\"\n    Write-Host (\"Elapsed: {0:c}\" -f $elapsed)\n\n    if ($exitCode -ne 0) {\n        exit $exitCode\n    }\n}\n"
  },
  {
    "path": "src/.aspire/settings.json",
    "content": "{\n  \"appHostPath\": \"../Grace.Aspire.AppHost/Grace.Aspire.AppHost.csproj\"\n}"
  },
  {
    "path": "src/.dockerignore",
    "content": "**/.classpath\n**/.dockerignore\n**/.env\n**/.git\n**/.gitignore\n**/.project\n**/.settings\n**/.toolstarget\n**/.vs\n**/.vscode\n**/*.*proj.user\n**/*.dbmdl\n**/*.jfm\n**/azds.yaml\n**/bin\n**/charts\n**/docker-compose*\n**/Dockerfile*\n**/node_modules\n**/npm-debug.log\n**/obj\n**/secrets.dev.yaml\n**/values.dev.yaml\nLICENSE\nREADME.md"
  },
  {
    "path": "src/.editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.{fs,fsi,fsproj}]\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nmax_line_length = 160\nfsharp_max_function_binding_width = 160\nfsharp_max_if_then_short_width = 80\nfsharp_max_if_then_else_short_width = 80\nfsharp_max_record_width = 160\nfsharp_max_value_binding_width = 160\nfsharp_multiline_block_brackets_on_same_column = true\nfsharp_experimental_keep_indent_in_branch = true\nfsharp_align_function_signature_to_indentation = true\n\n[*.{cs,csproj}]\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nmax_line_length = 120\n\n[*.hbs]\ninsert_final_newline = false\n\n[*.json]\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\nmax_line_length = off\n\n[*.{yml,yaml}]\nindent_size = 2\ninsert_final_newline = true\n"
  },
  {
    "path": "src/.gitattributes",
    "content": "# Use bd merge for beads JSONL files\n.beads/issues.jsonl merge=beads\n\n# Ensure all text files use LF line endings\n* text=auto eol=lf\n\n# Explicit rules for common source files\n*.fs     text eol=lf\n*.fsi    text eol=lf\n*.fsproj text eol=lf\n*.cs     text eol=lf\n*.csproj text eol=lf\n*.ps1    text eol=lf\n*.psm1   text eol=lf\n*.yml    text eol=lf\n*.yaml   text eol=lf\n*.json   text eol=lf\n*.md     text eol=lf\n*.sh     text eol=lf\n\n# Binary files should never be modified\n*.png    binary\n*.jpg    binary\n*.jpeg   binary\n*.gif    binary\n*.ico    binary\n*.pdf    binary\n*.zip    binary\n*.dll    binary\n*.exe    binary\n"
  },
  {
    "path": "src/.github/copilot-instructions.md",
    "content": "# GitHub Copilot Instructions for Beads\n\n## Project Overview\n\n**beads** (command: `bd`) is a Git-backed issue tracker designed for AI-supervised coding workflows. We dogfood our own tool for all task tracking.\n\n**Key Features:**\n- Dependency-aware issue tracking\n- Auto-sync with Git via JSONL\n- AI-optimized CLI with JSON output\n- Built-in daemon for background operations\n- MCP server integration for Claude and other AI assistants\n\n## Tech Stack\n\n- **Language**: Go 1.21+\n- **Storage**: SQLite (internal/storage/sqlite/)\n- **CLI Framework**: Cobra\n- **Testing**: Go standard testing + table-driven tests\n- **CI/CD**: GitHub Actions\n- **MCP Server**: Python (integrations/beads-mcp/)\n\n## Coding Guidelines\n\n### Testing\n- Always write tests for new features\n- Use `BEADS_DB=/tmp/test.db` to avoid polluting production database\n- Run `go test -short ./...` before committing\n- Never create test issues in production DB (use temporary DB)\n\n### Code Style\n- Run `golangci-lint run ./...` before committing\n- Follow existing patterns in `cmd/bd/` for new commands\n- Add `--json` flag to all commands for programmatic use\n- Update docs when changing behavior\n- Grace contributors should run `dotnet tool run fantomas --recurse .` from `./src` before finishing a task; CI should enforce formatting.\n\n### Git Workflow\n- Always commit `.beads/issues.jsonl` with code changes\n- Run `bd sync` at end of work sessions\n- Install git hooks: `bd hooks install` (ensures DB ↔ JSONL consistency)\n\n## Issue Tracking with bd\n\n**CRITICAL**: This project uses **bd** for ALL task tracking. Do NOT create markdown TODO lists.\n\n### Essential Commands\n\n```bash\n# Find work\nbd ready --json                    # Unblocked issues\nbd stale --days 30 --json          # Forgotten issues\n\n# Create and manage\nbd create \"Title\" -t bug|feature|task -p 0-4 --json\nbd create \"Subtask\" --parent <epic-id> --json  # Hierarchical subtask\nbd update <id> --status in_progress --json\nbd close <id> --reason \"Done\" --json\n\n# Search\nbd list --status open --priority 1 --json\nbd show <id> --json\n\n# Sync (CRITICAL at end of session!)\nbd sync\n```\n\n### Workflow\n1. **Check ready work**: `bd ready --json`\n2. **Claim task**: `bd update <id> --status in_progress --json`\n3. **Work on it**: Implement, test, document\n4. **Discover new work?** `bd create \"Found bug\" -p 1 --deps discovered-from:<parent-id> --json`\n5. **Complete**: `bd close <id> --reason \"Done\" --json`\n6. **Sync**: `bd sync`\n\n### Priorities\n- `0` - Critical (security, data loss, broken builds)\n- `1` - High (major features, important bugs)\n- `2` - Medium (default, nice-to-have)\n- `3` - Low (polish, optimization)\n- `4` - Backlog (future ideas)\n\n## Project Structure\n\n```\nbeads/\n├── cmd/bd/              # CLI commands (add new commands here)\n├── internal/\n│   ├── types/           # Core data types\n│   └── storage/         # Storage layer\n│       └── sqlite/      # SQLite implementation\n├── integrations/\n│   └── beads-mcp/       # MCP server (Python)\n├── examples/            # Integration examples\n├── docs/                # Documentation\n└── .beads/\n    ├── beads.db         # SQLite database (DO NOT COMMIT)\n    └── issues.jsonl     # Git-synced issue storage\n```\n\n## Available Resources\n\n### MCP Server (Recommended)\nUse the beads MCP server for native function calls instead of shell commands:\n- Install: `pip install beads-mcp`\n- Functions: `mcp__beads__ready()`, `mcp__beads__create()`, etc.\n- See `integrations/beads-mcp/README.md`\n\n### Scripts\n- `./scripts/bump-version.sh <version> --commit` - Update all version files atomically\n- `./scripts/release.sh <version>` - Complete release workflow\n- `./scripts/update-homebrew.sh <version>` - Update Homebrew formula\n\n### Key Documentation\n- **AGENTS.md** - Comprehensive AI agent guide (detailed workflows, advanced features)\n- **AGENT_INSTRUCTIONS.md** - Development procedures, testing, releases\n- **README.md** - User-facing documentation\n- **docs/CLI_REFERENCE.md** - Complete command reference\n\n## CLI Help\nRun `bd <command> --help` to see all available flags for any command.\nFor example: `bd create --help` shows `--parent`, `--deps`, `--assignee`, etc.\n\n## Important Rules\n- ✅ Use bd for ALL task tracking\n- ✅ Always use `--json` flag for programmatic use\n- ✅ Run `bd sync` at end of sessions\n- ✅ Test with `BEADS_DB=/tmp/test.db`\n- ✅ Run `bd <cmd> --help` to discover available flags\n- ❌ Do NOT create markdown TODO lists\n- ❌ Do NOT create test issues in production DB\n- ❌ Do NOT commit `.beads/beads.db` (JSONL only)\n\n---\n\n**For detailed workflows and advanced features, see [AGENTS.md](../AGENTS.md)**\n\n- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools.\n- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first.\n- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it.\n"
  },
  {
    "path": "src/.github/workflows/deploy-to-app-service.yml",
    "content": "name: build-and-deploy-grace-server\n\non:\n  push:\n    branches:\n      - main\n      - develop\n  pull_request:\n    branches:\n      - main\n\nenv:\n  DOTNET_VERSION: \"10.0.x\"\n  ASPNETCORE_ENVIRONMENT: Development\n  DOTNET_ENVIRONMENT: Development\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup .NET\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: ${{ env.DOTNET_VERSION }}\n\n      - name: Restore\n        run: dotnet restore\n\n      - name: Build\n        run: dotnet build --configuration Release --no-restore\n\n      - name: Start Aspire host\n        run: |\n          set -e\n          export DOTNET_ENVIRONMENT=${{ env.DOTNET_ENVIRONMENT }}\n          nohup dotnet run --project Grace.Aspire.AppHost/Grace.Aspire.AppHost.csproj --no-build > aspire.log 2>&1 &\n          echo $! > aspire.pid\n          sleep 90\n\n      - name: Run tests\n        run: |\n          set -e\n          export DOTNET_ENVIRONMENT=${{ env.DOTNET_ENVIRONMENT }}\n          dotnet test Grace.Server.Tests/Grace.Server.Tests.fsproj --configuration Release --no-build\n\n      - name: Stop Aspire host\n        if: always()\n        run: |\n          if [ -f aspire.pid ]; then\n            kill $(cat aspire.pid) || true\n            rm aspire.pid\n          fi\n          docker container prune --force\n          docker volume prune --force\n\n      - name: Publish Grace.Server\n        run: dotnet publish Grace.Server/Grace.Server.csproj -c Release -o ${{ runner.temp }}/publish\n\n      - name: Upload build artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: grace-server-release\n          path: ${{ runner.temp }}/publish\n\n      - name: Upload Aspire log\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: aspire-host-log\n          path: aspire.log\n\n  deploy:\n    runs-on: ubuntu-latest\n    needs: build\n    if: github.ref == 'refs/heads/main' && github.event_name == 'push'\n    environment:\n      name: production\n\n    steps:\n      - name: Download artifact\n        uses: actions/download-artifact@v4\n        with:\n          name: grace-server-release\n          path: ./publish\n\n      - name: Setup Azure CLI\n        uses: azure/login@v1\n        with:\n          client-id: ${{ secrets.AZURE_CLIENT_ID }}\n          tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n\n      - name: Deploy to App Service\n        uses: azure/webapps-deploy@v2\n        with:\n          app-name: grace-server\n          package: ./publish\n\n      - name: Logout Azure CLI\n        if: always()\n        run: az logout\n\n"
  },
  {
    "path": "src/AGENTS.md",
    "content": "# Grace Repository Agents Guide\n\nAgents operating under `D:\\Source\\Grace\\src` should follow this playbook alongside the existing `AGENTS.md` in the repo root.\nTreat this file as the canonical high-level brief; each project folder contains an `AGENTS.md` with deeper context.\n\n## Local Commands\n\n- `pwsh ./scripts/bootstrap.ps1`\n- `pwsh ./scripts/validate.ps1 -Fast` (use `-Full` for Aspire integration tests)\n\nOptional: `pwsh ./scripts/install-githooks.ps1` to add a pre-commit `validate -Fast` hook.\n\n## Work Tracking\n\nDo not use beads/`bd` by default.\nTrack implementation progress in the task-specific plan file (for example `CodexPlan.md`) and keep git history granular.\n\n## Core Engineering Expectations\n\n- Make a multi-step plan for non-trivial work, keep edits focused, and leave code cleaner than you found it.\n- Validate changes with `pwsh ./scripts/validate.ps1 -Fast` (use `-Full` for Aspire integration coverage).\n- If running commands manually, use `dotnet build --configuration Release` and `dotnet test --no-build`.\n- Resolve all compilation errors before considering a task complete.\n- Run impacted tests for each task and fix failures introduced by your changes.\n- Create a new git commit after each completed task to keep review scope clear.\n- Write tests for new features and bug fixes; prioritize critical paths.\n- Document new public APIs with XML comments and update nearby `AGENTS.md`/docs when behavior changes.\n- Treat secrets with care, avoid logging PII, and preserve structured logging (including correlation IDs).\n- Favor existing helpers in `Grace.Shared` before adding new utilities.\n\n## Test Project Organization\n\n- `Grace.Server/*.Server.fs` should be primarily covered by `Grace.Server.Tests/*Server.Tests.fs`.\n- `Grace.CLI/Command/*.CLI.fs` should be primarily covered by `Grace.CLI.Tests/*.CLI.Tests.fs`.\n- `Grace.Types/*.Types.fs` should be covered by `Grace.Types.Tests/*.Types.Tests.fs`.\n- Keep auth-focused suites separate for now (`Grace.Authorization.Tests`, plus auth-specific files inside other test projects).\n- Prefer server-surface integration tests for actor behavior; avoid duplicating deep actor internals in server test files.\n\n## F# Coding Guidelines\n\n- Default to F# for new code unless stakeholders specify another language.\n- Use `task { }` for asynchronous workflows and keep side effects isolated.\n- Prefer immutable data, small pure functions, and explicit dependencies passed as parameters.\n- Prefer collections from `System.Collections.Generic` (for example `List<T>`, `Dictionary<K,V>`) over F#-specific collections unless pattern matching or discriminated unions are needed.\n- Apply the modern indexer syntax (`myList[0]`) for lists, arrays, and sequences; avoid the legacy `.[ ]` form.\n- Structure modules so domain types live in `Grace.Types`, shared helpers in `Grace.Shared`, and orchestration in the project-specific assembly.\n- Add lightweight comments only where control flow or transformations are non-obvious.\n- Format code with `dotnet tool run fantomas --recurse .` from `./src`.\n\n## Avoid FS3511 in Resumable Computation Expressions\n\nThese rules apply to `task { }` and `backgroundTask { }`.\n\n1. **Do not define `let rec` inside `task { }`.**\n2. **Avoid `for ... in ... do` loops inside `task { }`.**\n3. **Treat FS3511 warnings as regressions; do not suppress them.**\n\n## Agent-Friendly Context Practices\n\n- Start with relevant `AGENTS.md` files to load patterns, dependencies, and test strategy before broad code exploration.\n- Use these summaries to target only source files needed for implementation or verification.\n- When documenting new behavior, update the closest `AGENTS.md` so future agents inherit context quickly.\n\n## Collaboration and Communication\n\n- Summarize modifications clearly, cite file paths with 1-based line numbers, and call out remaining follow-ups/tests.\n- Coordinate cross-project changes across `Grace.Types`, `Grace.Shared`, `Grace.Server`, `Grace.Actors`, `Grace.CLI`, and `Grace.SDK`.\n- When adding capabilities, ensure matching tests exist and note any residual risk.\n"
  },
  {
    "path": "src/CountLines.ps1",
    "content": "$codeFiles = Get-ChildItem -Include *.cs,*.csproj,*.fs,*.fsproj,*.yml,*.yaml,*.md -File -Recurse\n$totalLines = 0\n$files = [ordered]@{}\nforeach ($codeFile in ($codeFiles | Where-Object {$_.FullName -notlike \"*\\obj\\*\" -and $_.FullName -notlike \"*\\bin\\*\" -and $_.FullName -notlike \"*\\.grace\\*\"})) {\n    $stream = $codeFile.OpenText()\n    $fileContents = $stream.ReadToEnd()\n    $stream.Close()\n\n    $lines = 0\n    foreach ($line in $fileContents.Split(\"`n\")) {\n        if (-not [System.String]::IsNullOrEmpty($line) -and -not [System.String]::IsNullOrWhiteSpace($line)) {\n            $lines += 1\n        }\n    }\n\n    $files.Add($codeFile.FullName, $lines)\n    $totalLines += $lines\n}\n\n$files | Format-Table -AutoSize\n\nWrite-Host -ForegroundColor Magenta \"Total lines: $totalLines.\"\n"
  },
  {
    "path": "src/Create-Grace-Objects.ps1",
    "content": "$startTime = Get-Date\n$iterations = 50\n\n1..$iterations | ForEach-Object -Parallel {\n    Set-Alias -Name grace -Value D:\\Source\\Grace\\src\\Grace.CLI\\bin\\Debug\\net8.0\\Grace.CLI.exe\n\n    $suffix = (Get-Random -Maximum 65536).ToString(\"X4\")\n\n    $ownerId = (New-Guid).ToString()\n    $ownerNameOriginal = 'Owner' + $suffix\n    $ownerName = 'Owner' + $suffix + 'A'\n    $organizationId = (New-Guid).ToString()\n    $orgNameOriginal = 'Org' + $suffix\n    $orgName = 'Org' + $suffix + 'A'\n    $repoId = (New-Guid).ToString()\n    $repoNameOriginal = 'Repo' + $suffix\n    $repoName = 'Repo' + $suffix + 'A'\n    $branchId = (New-Guid).ToString()\n    $branchName = 'Branch' + $suffix\n\n    grace owner create --output Verbose --ownerName $ownerNameOriginal --ownerId $ownerId --doNotSwitch\n    grace owner set-name --output Verbose --ownerId $ownerId --newName $ownerName\n    grace owner get --output Verbose --ownerId $ownerId\n    grace org create --output Verbose --ownerId $ownerId --organizationName $orgNameOriginal --organizationId $organizationId --doNotSwitch\n    grace org set-name --output Verbose --ownerId $ownerId --organizationId $organizationId --newName $orgName\n    grace org get --output Verbose --ownerId $ownerId --organizationId $organizationId\n    grace repo create --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryName $repoNameOriginal --repositoryId $repoId --doNotSwitch\n    grace repo set-name --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --newName $repoName\n    grace repo get --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId\n    grace branch create --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --branchId $branchId --branchName $branchName --parentBranchName main --doNotSwitch\n\n    $words = \"Sit fusce at sociosqu eros bibendum aliquet cursus ante non facilisis tempor Scelerisque arcu potenti feugiat fermentum viverra et litora facilisis vestibulum sit aliquam quisque sagittis ut Ultricies nisi urna cursus tellus tempor vivamus nec Dictumst tristique porta vel cubilia mollis Tempus nullam laoreet sit vestibulum etiam in volutpat dui class netus morbi Duis facilisis at aliquet fusce nisi Nulla arcu molestie mauris integer aenean ligula curabitur dui sociosqu suspendisse mi fringilla faucibus Rhoncus habitasse massa amet ipsum ligula quisque Quisque fames bibendum eu ullamcorper pulvinar in aenean hendrerit Augue tristique aenean amet auctor curabitur congue placerat aenean posuere porttitor pulvinar lectus Mattis aenean elit condimentum nam iaculis ante felis sollicitudin Risus viverra ornare curabitur sem massa nibh vulputate senectus dictum vitae leo varius dictumst tristique Ultrices ut blandit adipiscing dictumst sagittis elementum urna Vel feugiat consectetur malesuada nibh turpis odio convallis molestie vulputate magna venenatis lacinia Suscipit consequat lectus nullam suspendisse aliquam sed venenatis Feugiat vehicula iaculis donec aenean Volutpat amet feugiat fringilla bibendum scelerisque fermentum pellentesque hendrerit dapibus primis eu ipsum proin mauris Amet magna non mattis dictum risus sit Luctus hendrerit in integer euismod sapien aenean vel maecenas venenatis lorem cubilia taciti Id mauris dictum aenean leo quisque auctor sagittis nisl rutrum at Iaculis luctus orci egestas metus commodo praesent sodales nam quis conubia cras sagittis vestibulum Viverra justo cursus tempor fringilla egestas Potenti aliquam quisque tincidunt pellentesque Lacinia eu convallis quis risus accumsan Augue adipiscing orci massa lorem curabitur eleifend tincidunt justo varius vulputate Mollis aenean est pulvinar proin in donec bibendum dolor quis sociosqu mattis mi Euismod urna leo mollis potenti fames mattis ultrices diam Vivamus sit mattis vehicula viverra mi imperdiet Adipiscing est vehicula scelerisque velit Malesuada integer quisque fusce quis mollis eros Leo nec tellus curabitur ornare amet quisque fusce habitasse morbi Sem lacinia eu aenean pretium curae dolor cubilia faucibus purus Sollicitudin nisl tempus auctor etiam felis urna consectetur donec dui Posuere elit orci lobortis magna Enim at pellentesque ac taciti convallis sapien ad elit Integer potenti malesuada lacinia fames euismod amet purus justo sociosqu dolor cras tempus dictumst Dictumst adipiscing quisque sapien pharetra pretium aliquam nunc ipsum varius mi justo aenean mattis Aenean conubia felis inceptos nulla ante sociosqu libero non imperdiet Nunc feugiat sodales commodo interdum rhoncus nulla aliquet cras sociosqu eros sed Vivamus varius sapien sollicitudin curabitur class aenean tempus tempor magna donec bibendum nulla morbi semper Praesent inceptos etiam tempus in Varius hac et feugiat nullam dictum vivamus adipiscing ut in eros nulla molestie ante Interdum dictum volutpat accumsan posuere quis amet curae nostra purus fusce nisl lacus Aenean erat suscipit urna ante In ad varius interdum porta at pulvinar aptent enim nam sit ultrices hendrerit Vitae rhoncus consequat non metus nullam augue Massa vestibulum dapibus lectus nibh at tortor ullamcorper mattis rutrum pellentesque aliquam adipiscing porttitor\".Split()\n\n    # 1..10 | ForEach-Object {\n    #     $numberOfWords = Get-Random -Minimum 3 -Maximum 9\n    #     $start = Get-Random -Minimum 0 -Maximum ($words.Count - $numberOfWords)\n    #     $message = ''\n    #     for ($i = $0; $i -lt $numberOfWords; $i++) {\n    #         $message += $words[$i + $start] + \" \"\n    #     }\n\n        # switch (Get-Random -Maximum 4) {\n        #     0 {grace branch save --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --branchName $branchName}\n        #     1 {grace branch checkpoint --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --branchName $branchName -m $message}\n        #     2 {grace branch commit --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --branchName $branchName -m $message}\n        #     3 {grace branch tag --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --branchName $branchName -m $message}\n        # }\n    # }\n\n    grace branch delete --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --branchId $branchId\n    grace repo delete --output Verbose --ownerId $ownerId --organizationId $organizationId --repositoryId $repoId --deleteReason \"Test cleanup\"\n    grace org delete --output Verbose --ownerId $ownerId --organizationId $organizationId --deleteReason \"Test cleanup\"\n    grace owner delete --output Verbose --ownerId $ownerId --deleteReason \"Test cleanup\"\n} -ThrottleLimit 16\n\n$endTime = Get-Date\n$elapsed = $endTime.Subtract($startTime).TotalSeconds\n\"Elapsed: $($elapsed.ToString('F3')) seconds; Operations: $($iterations * 14); Operations/second: $($iterations * 14 / $elapsed).\"\n"
  },
  {
    "path": "src/Directory.Build.props",
    "content": "<Project>\n    <PropertyGroup>\n        <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <ProduceReferenceAssembly>true</ProduceReferenceAssembly>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <EnableNETAnalyzers>true</EnableNETAnalyzers>\n        <AnalysisLevel>latest</AnalysisLevel>\n        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n        <NoWarn>$(NoWarn);NETSDK1057</NoWarn>\n        <PublishProfile>DefaultContainer</PublishProfile>\n        <OtherFlags>$(OtherFlags) --test:GraphBasedChecking</OtherFlags>\n    </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.Actors/AGENTS.md",
    "content": "# Grace.Actors Agents Guide\n\nStart with `../AGENTS.md` for global rules before working on Orleans code.\n\n## Purpose\n- Define Orleans grain interfaces and implementations that orchestrate stateful workflows in Grace.\n- Persist state through domain events and records defined in `Grace.Types`.\n\n## Key Patterns\n- Follow standard Orleans activation patterns; keep grain constructors light and rely on dependency injection.\n- Drive state changes through explicit events or commands; keep transitions deterministic and idempotent.\n- Use domain types from `Grace.Types` directly to avoid duplication and guarantee serialization compatibility.\n- Separate orchestration (grains) from external service or SDK calls by delegating to helper modules/services.\n\n## Project Rules\n1. When adding new grain types or serializer changes, ensure `Grace.Orleans.CodeGen` stays in sync and regenerates as needed.\n2. Keep grain state mutations safe for retries—idempotent transitions make distributed recovery predictable.\n3. Document non-obvious activation, reminder, or timer behavior here so future agents can reason without scanning the entire implementation.\n\n## Validation\n- Add activation and idempotency tests for new grains or state transitions.\n- Run `dotnet test --no-build` focusing on actor-related fixtures, then smoke `dotnet build --configuration Release` for the solution.\n- Confirm code generation outputs (if applicable) before finalizing a PR.\n"
  },
  {
    "path": "src/Grace.Actors/AccessControl.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule AccessControl =\n\n    let ActorName = ActorName.AccessControl\n\n    [<GenerateSerializer>]\n    type AccessControlState = { Assignments: RoleAssignment list }\n\n    module AccessControlState =\n        let Empty = { Assignments = [] }\n\n    let getScopeKey (scope: Scope) =\n        match scope with\n        | Scope.System -> \"system\"\n        | Scope.Owner ownerId -> $\"owner:{ownerId}\"\n        | Scope.Organization (ownerId, organizationId) -> $\"org:{ownerId}:{organizationId}\"\n        | Scope.Repository (ownerId, organizationId, repositoryId) -> $\"repo:{ownerId}:{organizationId}:{repositoryId}\"\n        | Scope.Branch (ownerId, organizationId, repositoryId, branchId) -> $\"branch:{ownerId}:{organizationId}:{repositoryId}:{branchId}\"\n\n    type AccessControlActor([<PersistentState(StateName.AccessControl, Grace.Shared.Constants.GraceActorStorage)>] state: IPersistentState<AccessControlState>) =\n        inherit Grain()\n\n        let log = loggerFactory.CreateLogger(\"AccessControl.Actor\")\n\n        let mutable accessControlState = AccessControlState.Empty\n        let mutable correlationId: CorrelationId = String.Empty\n\n        override this.OnActivateAsync(ct) =\n            task {\n                accessControlState <- if state.RecordExists then state.State else AccessControlState.Empty\n\n                let isSystemScope =\n                    this.GetPrimaryKeyString()\n                        .Equals(getScopeKey Scope.System, StringComparison.OrdinalIgnoreCase)\n\n                if isSystemScope && accessControlState.Assignments.IsEmpty then\n                    let parseList (value: string) =\n                        if String.IsNullOrWhiteSpace value then\n                            []\n                        else\n                            value.Split(';', StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries)\n                            |> Seq.map (fun item -> item.Trim())\n                            |> Seq.filter (fun item -> not (String.IsNullOrWhiteSpace item))\n                            |> Seq.distinct\n                            |> Seq.toList\n\n                    let bootstrapUsers =\n                        Environment.GetEnvironmentVariable(EnvironmentVariables.GraceAuthzBootstrapSystemAdminUsers)\n                        |> parseList\n\n                    let bootstrapGroups =\n                        Environment.GetEnvironmentVariable(EnvironmentVariables.GraceAuthzBootstrapSystemAdminGroups)\n                        |> parseList\n\n                    if (not bootstrapUsers.IsEmpty) || (not bootstrapGroups.IsEmpty) then\n                        let now = getCurrentInstant ()\n                        let sourceDetailParts = List<string>()\n\n                        if not bootstrapUsers.IsEmpty then\n                            sourceDetailParts.Add($\"users={String.Join(';', bootstrapUsers)}\")\n\n                        if not bootstrapGroups.IsEmpty then\n                            sourceDetailParts.Add($\"groups={String.Join(';', bootstrapGroups)}\")\n\n                        let sourceDetail =\n                            if sourceDetailParts.Count = 0 then\n                                None\n                            else\n                                Some(String.Join(\"; \", sourceDetailParts))\n\n                        let userAssignments =\n                            bootstrapUsers\n                            |> List.map (fun userId ->\n                                {\n                                    Principal = { PrincipalType = PrincipalType.User; PrincipalId = userId }\n                                    Scope = Scope.System\n                                    RoleId = \"SystemAdmin\"\n                                    Source = \"bootstrap\"\n                                    SourceDetail = sourceDetail\n                                    CreatedAt = now\n                                })\n\n                        let groupAssignments =\n                            bootstrapGroups\n                            |> List.map (fun groupId ->\n                                {\n                                    Principal = { PrincipalType = PrincipalType.Group; PrincipalId = groupId }\n                                    Scope = Scope.System\n                                    RoleId = \"SystemAdmin\"\n                                    Source = \"bootstrap\"\n                                    SourceDetail = sourceDetail\n                                    CreatedAt = now\n                                })\n\n                        accessControlState <- { accessControlState with Assignments = userAssignments @ groupAssignments }\n                        do! this.SaveState()\n\n                        log.LogWarning(\n                            \"{CurrentInstant}: Bootstrapped {UserCount} system admin user(s) and {GroupCount} group(s) for system scope.\",\n                            getCurrentInstantExtended (),\n                            userAssignments.Length,\n                            groupAssignments.Length\n                        )\n\n                return ()\n            }\n            :> Task\n\n        member private this.ValidateScope (scope: Scope) (correlationId: CorrelationId) =\n            let expectedKey = getScopeKey scope\n            let actualKey = this.GetPrimaryKeyString()\n\n            if expectedKey = actualKey then\n                Ok()\n            else\n                Error(GraceError.Create $\"AccessControl scope mismatch. Expected '{expectedKey}', got '{actualKey}'.\" correlationId)\n\n        member private this.SaveState() =\n            task {\n                state.State <- accessControlState\n\n                if accessControlState.Assignments |> List.isEmpty then\n                    do! DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> state.ClearStateAsync())\n                else\n                    do! DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> state.WriteStateAsync())\n            }\n\n        member private this.GrantRole (assignment: RoleAssignment) (metadata: EventMetadata) =\n            task {\n                correlationId <- metadata.CorrelationId\n\n                match this.ValidateScope assignment.Scope metadata.CorrelationId with\n                | Error error -> return Error error\n                | Ok _ ->\n                    let assignments =\n                        accessControlState.Assignments\n                        |> List.filter (fun existing ->\n                            not (\n                                existing.Principal = assignment.Principal\n                                && existing.RoleId.Equals(assignment.RoleId, StringComparison.OrdinalIgnoreCase)\n                            ))\n\n                    accessControlState <- { accessControlState with Assignments = assignment :: assignments }\n                    do! this.SaveState()\n\n                    let returnValue = GraceReturnValue.Create accessControlState.Assignments metadata.CorrelationId\n                    return Ok returnValue\n            }\n\n        member private this.RevokeRole (principal: Principal) (roleId: RoleId) (metadata: EventMetadata) =\n            task {\n                correlationId <- metadata.CorrelationId\n\n                let assignments =\n                    accessControlState.Assignments\n                    |> List.filter (fun existing ->\n                        not (\n                            existing.Principal = principal\n                            && existing.RoleId.Equals(roleId, StringComparison.OrdinalIgnoreCase)\n                        ))\n\n                accessControlState <- { accessControlState with Assignments = assignments }\n                do! this.SaveState()\n\n                let returnValue = GraceReturnValue.Create accessControlState.Assignments metadata.CorrelationId\n                return Ok returnValue\n            }\n\n        member private this.ListAssignments (principal: Principal option) (metadata: EventMetadata) =\n            task {\n                correlationId <- metadata.CorrelationId\n\n                let filtered =\n                    match principal with\n                    | None -> accessControlState.Assignments\n                    | Some value ->\n                        accessControlState.Assignments\n                        |> List.filter (fun assignment -> assignment.Principal = value)\n\n                let returnValue = GraceReturnValue.Create filtered metadata.CorrelationId\n                return Ok returnValue\n            }\n\n        interface IAccessControlActor with\n            member this.Handle command metadata =\n                match command with\n                | AccessControlCommand.GrantRole assignment -> this.GrantRole assignment metadata\n                | AccessControlCommand.RevokeRole (principal, roleId) -> this.RevokeRole principal roleId metadata\n                | AccessControlCommand.ListAssignments principal -> this.ListAssignments principal metadata\n\n            member this.GetAssignments principal correlationId =\n                let filtered =\n                    match principal with\n                    | None -> accessControlState.Assignments\n                    | Some value ->\n                        accessControlState.Assignments\n                        |> List.filter (fun assignment -> assignment.Principal = value)\n\n                filtered |> returnTask\n"
  },
  {
    "path": "src/Grace.Actors/ActorProxy.Extensions.Actor.fs",
    "content": "namespace Grace.Actors.Extensions\n\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Timing\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\n\nmodule ActorProxy =\n\n    let getGrainIdentity (grainId: GrainId) = $\"{grainId.Type}/{grainId.Key}\"\n\n    type Orleans.IGrainFactory with\n        /// Creates an Orleans grain reference for the given interface and actor type, and adds the correlationId to the grain's context.\n        member this.CreateActorProxyWithCorrelationId<'T when 'T :> IGrainWithGuidKey>(primaryKey: Guid, correlationId) =\n            //logToConsole $\"Creating grain for {typeof<'T>.Name} with primary key: {primaryKey}.\"\n            RequestContext.Set(Constants.CorrelationId, correlationId)\n            let grain = orleansClient.GetGrain<'T>(primaryKey)\n            //logToConsole $\"Created actor proxy: CorrelationId: {correlationId}; ActorType: {typeof<'T>.Name}; GrainIdentity: {grain.GetGrainId()}.\"\n            grain\n\n        member this.CreateActorProxyWithCorrelationId<'T when 'T :> IGrainWithStringKey>(primaryKey: String, correlationId) =\n            //logToConsole $\"Creating grain for {typeof<'T>.Name} with primary key: {primaryKey}.\"\n            RequestContext.Set(Constants.CorrelationId, correlationId)\n            let grain = orleansClient.GetGrain<'T>(primaryKey)\n            //logToConsole $\"Created actor proxy: CorrelationId: {correlationId}; ActorType: {typeof<'T>.Name}; GrainIdentity: {grain.GetGrainId()}.\"\n            grain\n\n    module Branch =\n        /// Creates an ActorProxy for a Branch actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (branchId: BranchId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IBranchActor>(branchId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Branch)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module BranchName =\n        /// Creates an ActorProxy for a BranchName actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (repositoryId: RepositoryId) (branchName: BranchName) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IBranchNameActor>($\"{repositoryId}|{branchName}\", correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.BranchName)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Diff =\n        /// Gets an ActorId for a Diff actor.\n        let GetPrimaryKey (directoryVersionId1: DirectoryVersionId) (directoryVersionId2: DirectoryVersionId) =\n            if directoryVersionId1 < directoryVersionId2 then\n                $\"{directoryVersionId1}*{directoryVersionId2}\"\n            else\n                $\"{directoryVersionId2}*{directoryVersionId1}\"\n\n        /// Creates an ActorProxy for a Diff actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy\n            (directoryVersionId1: DirectoryVersionId)\n            (directoryVersionId2: DirectoryVersionId)\n            (ownerId: OwnerId)\n            (organizationId: OrganizationId)\n            (repositoryId: RepositoryId)\n            correlationId\n            =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IDiffActor>((GetPrimaryKey directoryVersionId1 directoryVersionId2), correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Diff)\n            orleansContext.Add(nameof OwnerId, ownerId)\n            orleansContext.Add(nameof OrganizationId, organizationId)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module DirectoryVersion =\n        /// Creates an ActorProxy for a DirectoryVersion actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (directoryVersionId: DirectoryVersionId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IDirectoryVersionActor>(directoryVersionId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.DirectoryVersion)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module DirectoryAppearance =\n        /// Creates an ActorProxy for a DirectoryAppearance actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (directoryVersionId: DirectoryVersionId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IDirectoryAppearanceActor>(directoryVersionId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.DirectoryAppearance)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module FileAppearance =\n        /// Creates an ActorProxy for a FileAppearance actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (fileVersionWithRelativePath: string) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IFileAppearanceActor>(fileVersionWithRelativePath, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.FileAppearance)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module GlobalLock =\n        /// Creates an ActorProxy for a GlobalLock actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (lockId: string) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IGlobalLockActor>(lockId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.GlobalLock)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Organization =\n        /// Creates an ActorProxy for an Organization actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (organizationId: OrganizationId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IOrganizationActor>(organizationId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Organization)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module OrganizationName =\n        /// Creates an ActorProxy for an OrganizationName actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (ownerId: OwnerId) (organizationName: OrganizationName) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IOrganizationNameActor>($\"{ownerId}|{organizationName}\", correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.OrganizationName)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Owner =\n        /// Creates an ActorProxy for an Owner actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (ownerId: OwnerId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IOwnerActor>(ownerId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.OwnerName)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module OwnerName =\n        /// Creates an ActorProxy for an OwnerName actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (ownerName: OwnerName) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IOwnerNameActor>(ownerName, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.OwnerName)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module PersonalAccessToken =\n        /// Creates an ActorProxy for a PersonalAccessToken actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (userId: string) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IPersonalAccessTokenActor>(userId, correlationId)\n\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.PersonalAccessToken)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Reminder =\n        /// Creates an ActorProxy for a Reminder actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (reminderId: ReminderId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IReminderActor>(reminderId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Reminder)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Reference =\n        /// Creates an ActorProxy for a Reference actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (referenceId: ReferenceId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IReferenceActor>(referenceId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Reference)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Repository =\n        /// Creates an ActorProxy for a Repository actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (organizationId: OrganizationId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IRepositoryActor>(repositoryId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof OrganizationId, organizationId)\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Repository)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module RepositoryName =\n        /// Gets an ActorId for a RepositoryName actor.\n        let GetPrimaryKey (ownerId: OwnerId) (organizationId: OrganizationId) (repositoryName: RepositoryName) = $\"{repositoryName}|{ownerId}|{organizationId}\"\n\n        /// Creates an ActorProxy for a RepositoryName actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (ownerId: OwnerId) (organizationId: OrganizationId) (repositoryName: RepositoryName) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IRepositoryNameActor>($\"{ownerId}|{organizationId}|{repositoryName}\", correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof OrganizationId, organizationId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.RepositoryName)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module PromotionQueue =\n        open Grace.Types.Queue\n\n        /// Creates an ActorProxy for a PromotionQueue actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (targetBranchId: BranchId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IPromotionQueueActor>(targetBranchId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.PromotionQueue)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module PromotionSet =\n        open Grace.Types.PromotionSet\n\n        /// Creates an ActorProxy for a PromotionSet actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (promotionSetId: PromotionSetId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IPromotionSetActor>(promotionSetId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.PromotionSet)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module ValidationSet =\n        open Grace.Types.Validation\n\n        /// Creates an ActorProxy for a ValidationSet actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (validationSetId: ValidationSetId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IValidationSetActor>(validationSetId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.ValidationSet)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module ValidationResult =\n        open Grace.Types.Validation\n\n        /// Creates an ActorProxy for a ValidationResult actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (validationResultId: ValidationResultId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IValidationResultActor>(validationResultId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.ValidationResult)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Artifact =\n        open Grace.Types.Artifact\n\n        /// Creates an ActorProxy for an Artifact actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (artifactId: ArtifactId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IArtifactActor>(artifactId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Artifact)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Policy =\n        open Grace.Types.Policy\n\n        /// Creates an ActorProxy for a Policy actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (targetBranchId: BranchId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IPolicyActor>(targetBranchId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Policy)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module Review =\n        open Grace.Types.Review\n\n        /// Creates an ActorProxy for a Review actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (promotionSetId: PromotionSetId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IReviewActor>(promotionSetId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.Review)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module WorkItem =\n        open Grace.Types.WorkItem\n\n        /// Creates an ActorProxy for a WorkItem actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (workItemId: WorkItemId) (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IWorkItemActor>(workItemId, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.WorkItem)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module WorkItemNumber =\n        /// Creates an ActorProxy for a WorkItemNumber actor. The primary key is repository-scoped.\n        let CreateActorProxy (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IWorkItemNumberActor>($\"{repositoryId}\", correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.WorkItemNumber)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module WorkItemNumberCounter =\n        /// Creates an ActorProxy for a WorkItemNumberCounter actor. The primary key is repository-scoped.\n        let CreateActorProxy (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IWorkItemNumberCounterActor>($\"{repositoryId}\", correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.WorkItemNumberCounter)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module AccessControl =\n        /// Creates an ActorProxy for an AccessControl actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (scopeKey: string) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IAccessControlActor>(scopeKey, correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.AccessControl)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n\n    module RepositoryPermission =\n        /// Creates an ActorProxy for a RepositoryPermission actor, and adds the correlationId to the server's MemoryCache so\n        ///   it's available in the OnActivateAsync() method.\n        let CreateActorProxy (repositoryId: RepositoryId) (correlationId: string) =\n            let grain = orleansClient.CreateActorProxyWithCorrelationId<IRepositoryPermissionActor>($\"{repositoryId}\", correlationId)\n            let orleansContext = Dictionary<string, obj>()\n            orleansContext.Add(nameof RepositoryId, repositoryId)\n            orleansContext.Add(Constants.ActorNameProperty, ActorName.RepositoryPermission)\n            memoryCache.CreateOrleansContextEntry(grain.GetGrainId(), orleansContext)\n            grain\n"
  },
  {
    "path": "src/Grace.Actors/Artifact.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Artifact\nopen Grace.Types.Events\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule Artifact =\n\n    type ArtifactActor([<PersistentState(StateName.Artifact, Constants.GraceActorStorage)>] state: IPersistentState<List<ArtifactEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Artifact\n        let log = loggerFactory.CreateLogger(\"Artifact.Actor\")\n\n        let mutable currentCommand = String.Empty\n        let mutable artifact = ArtifactMetadata.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            artifact <-\n                state.State\n                |> Seq.fold (fun dto event -> ArtifactMetadata.UpdateDto event dto) artifact\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent(artifactEvent: ArtifactEvent) =\n            task {\n                let correlationId = artifactEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(artifactEvent)\n                    do! state.WriteStateAsync()\n\n                    artifact <-\n                        artifact\n                        |> ArtifactMetadata.UpdateDto artifactEvent\n\n                    let graceEvent = GraceEvent.ArtifactEvent artifactEvent\n                    do! publishGraceEvent graceEvent artifactEvent.Metadata\n\n                    let graceReturnValue: GraceReturnValue<string> =\n                        (GraceReturnValue.Create \"Artifact command succeeded.\" correlationId)\n                            .enhance(nameof RepositoryId, artifact.RepositoryId)\n                            .enhance (nameof ArtifactId, artifact.ArtifactId)\n\n                    return Ok graceReturnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to apply event for ArtifactId: {ArtifactId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        artifact.ArtifactId\n                    )\n\n                    return\n                        Error(\n                            (GraceError.CreateWithException ex \"Failed while applying Artifact event.\" correlationId)\n                                .enhance(nameof RepositoryId, artifact.RepositoryId)\n                                .enhance (nameof ArtifactId, artifact.ArtifactId)\n                        )\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = artifact.RepositoryId |> returnTask\n\n        interface IArtifactActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                not\n                <| artifact.ArtifactId.Equals(ArtifactId.Empty)\n                |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n\n                if artifact.ArtifactId = ArtifactId.Empty then Option.None else Some artifact\n                |> returnTask\n\n            member this.GetEvents correlationId =\n                this.correlationId <- correlationId\n\n                state.State :> IReadOnlyList<ArtifactEvent>\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (artifactCommand: ArtifactCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = eventMetadata.CorrelationId) then\n                            return Error(GraceError.Create \"Duplicate correlation ID for Artifact command.\" eventMetadata.CorrelationId)\n                        else\n                            match artifactCommand with\n                            | ArtifactCommand.Create _ when artifact.ArtifactId <> ArtifactId.Empty ->\n                                return Error(GraceError.Create \"Artifact already exists.\" eventMetadata.CorrelationId)\n                            | _ -> return Ok artifactCommand\n                    }\n\n                let processCommand (artifactCommand: ArtifactCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        let eventType =\n                            match artifactCommand with\n                            | ArtifactCommand.Create artifactDto -> ArtifactEventType.Created artifactDto\n\n                        let artifactEvent: ArtifactEvent = { Event = eventType; Metadata = eventMetadata }\n                        return! this.ApplyEvent artifactEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n\n                    match! isValid command metadata with\n                    | Ok validCommand -> return! processCommand validCommand metadata\n                    | Error validationError -> return Error validationError\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Branch.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Reference\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.Branch\nopen Grace.Types.Events\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Globalization\nopen System.Linq\nopen System.Runtime.Serialization\nopen System.Text\nopen System.Threading.Tasks\nopen System.Text.Json\nopen System.Net.Http.Json\nopen FSharpPlus.Data.MultiMap\nopen System.Threading\n\nmodule Branch =\n\n    type BranchActor([<PersistentState(StateName.Branch, Constants.GraceActorStorage)>] state: IPersistentState<List<BranchEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Branch\n\n        let log = loggerFactory.CreateLogger(\"Branch.Actor\")\n\n        let mutable branchDto: BranchDto = BranchDto.Default\n\n        let mutable currentCommand = String.Empty\n\n        /// Updates the branchDto with the latest reference of each type from the branch.\n        let updateLatestReferences (branchDto: BranchDto) correlationId =\n            task {\n                let mutable newBranchDto = branchDto\n\n                // Get the enabled reference types. This allows us to limit the ReferenceTypes we search for.\n                let enabledReferenceTypes = List<ReferenceType>()\n\n                if branchDto.PromotionEnabled then\n                    enabledReferenceTypes.Add(ReferenceType.Promotion)\n\n                if branchDto.CommitEnabled then enabledReferenceTypes.Add(ReferenceType.Commit)\n\n                if branchDto.CheckpointEnabled then\n                    enabledReferenceTypes.Add(ReferenceType.Checkpoint)\n\n                if branchDto.SaveEnabled then enabledReferenceTypes.Add(ReferenceType.Save)\n                if branchDto.TagEnabled then enabledReferenceTypes.Add(ReferenceType.Tag)\n\n                if branchDto.ExternalEnabled then\n                    enabledReferenceTypes.Add(ReferenceType.External)\n\n                if branchDto.AutoRebaseEnabled then\n                    enabledReferenceTypes.Add(ReferenceType.Rebase)\n\n                let referenceTypes = enabledReferenceTypes.ToArray()\n\n                // Get the latest references.\n                let! latestReferences = getLatestReferenceByReferenceTypes referenceTypes branchDto.RepositoryId branchDto.BranchId\n\n                // Get the latest reference of any type.\n                let latestReference =\n                    latestReferences\n                        .Values\n                        .OrderByDescending(fun referenceDto -> referenceDto.UpdatedAt)\n                        .FirstOrDefault(ReferenceDto.Default)\n\n                newBranchDto <- { newBranchDto with LatestReference = latestReference }\n\n                // Get the latest reference of each type.\n                for kvp in latestReferences do\n                    let referenceDto = kvp.Value\n\n                    match kvp.Key with\n                    | Save -> newBranchDto <- { newBranchDto with LatestSave = referenceDto }\n                    | Checkpoint -> newBranchDto <- { newBranchDto with LatestCheckpoint = referenceDto }\n                    | Commit -> newBranchDto <- { newBranchDto with LatestCommit = referenceDto }\n                    | Promotion -> newBranchDto <- { newBranchDto with LatestPromotion = referenceDto; BasedOn = referenceDto }\n                    | Rebase ->\n                        let basedOnLink =\n                            kvp.Value.Links\n                            |> Seq.find (fun link ->\n                                match link with\n                                | ReferenceLinkType.BasedOn _ -> true\n                                | _ -> false)\n\n                        let basedOnReferenceId =\n                            match basedOnLink with\n                            | ReferenceLinkType.BasedOn referenceId -> referenceId\n                            | _ -> ReferenceId.Empty\n\n                        let basedOnReferenceActorProxy = Reference.CreateActorProxy basedOnReferenceId branchDto.RepositoryId correlationId\n                        let! basedOnReferenceDto = basedOnReferenceActorProxy.Get correlationId\n\n                        newBranchDto <- { newBranchDto with BasedOn = basedOnReferenceDto }\n                    | External -> ()\n                    | Tag -> ()\n\n                return { newBranchDto with ShouldRecomputeLatestReferences = false }\n            }\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            branchDto <-\n                state.State\n                |> Seq.fold (fun branchDto branchEvent -> branchDto |> BranchDto.UpdateDto branchEvent) BranchDto.Default\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent branchEvent =\n            task {\n                try\n                    // If the branchEvent is Created or Rebased, we need to get the reference that the branch is based on for updating the branchDto.\n                    match branchEvent.Event with\n                    | Created (branchId, branchName, parentBranchId, basedOn, ownerId, organizationId, repositoryId, branchPermissions) ->\n                        let! basedOnReferenceDto =\n                            if basedOn <> ReferenceId.Empty then\n                                task {\n                                    let referenceActorProxy = Reference.CreateActorProxy basedOn repositoryId branchEvent.Metadata.CorrelationId\n                                    return! referenceActorProxy.Get branchEvent.Metadata.CorrelationId\n                                }\n                            else\n                                ReferenceDto.Default |> returnTask\n\n                        branchEvent.Metadata.Properties[ \"basedOnReferenceDto\" ] <- serialize basedOnReferenceDto\n                    | Rebased basedOn ->\n                        let referenceActorProxy = Reference.CreateActorProxy basedOn branchDto.RepositoryId branchEvent.Metadata.CorrelationId\n                        let! basedOnReferenceDto = referenceActorProxy.Get branchEvent.Metadata.CorrelationId\n                        branchEvent.Metadata.Properties[ \"basedOnReferenceDto\" ] <- serialize basedOnReferenceDto\n                    | _ -> ()\n\n                    // Update the branchDto with the event.\n                    branchDto <- branchDto |> BranchDto.UpdateDto branchEvent\n                    branchEvent.Metadata.Properties[ nameof RepositoryId ] <- $\"{branchDto.RepositoryId}\"\n\n                    match branchEvent.Event with\n                    // Don't save these reference creation events, and don't send them as events; that was done by the Reference actor when the reference was created.\n                    | Assigned (referenceDto, _, _, _)\n                    | Promoted (referenceDto, _, _, _)\n                    | Committed (referenceDto, _, _, _)\n                    | Checkpointed (referenceDto, _, _, _)\n                    | Saved (referenceDto, _, _, _)\n                    | Tagged (referenceDto, _, _, _)\n                    | ExternalCreated (referenceDto, _, _, _) -> branchEvent.Metadata.Properties[ nameof ReferenceId ] <- $\"{referenceDto.ReferenceId}\"\n                    | Rebased referenceId -> branchEvent.Metadata.Properties[ nameof ReferenceId ] <- $\"{referenceId}\"\n                    // Save the rest of the events.\n                    | _ ->\n                        // For all other events, add the event to the branchEvents list, and save it to actor state.\n                        state.State.Add branchEvent\n                        do! state.WriteStateAsync()\n\n                        // Publish the event to the rest of the world.\n                        let graceEvent = GraceEvent.BranchEvent branchEvent\n                        do! publishGraceEvent graceEvent branchEvent.Metadata\n\n                    let returnValue = GraceReturnValue.Create \"Branch command succeeded.\" branchEvent.Metadata.CorrelationId\n\n                    returnValue\n                        .enhance(nameof RepositoryId, branchDto.RepositoryId)\n                        .enhance(nameof BranchId, branchDto.BranchId)\n                        .enhance(nameof BranchName, branchDto.BranchName)\n                        .enhance(nameof ParentBranchId, branchDto.ParentBranchId)\n                        .enhance (nameof BranchEventType, getDiscriminatedUnionFullName branchEvent.Event)\n                    |> ignore\n\n                    // If the event has a referenceId, add it to the return properties.\n                    if branchEvent.Metadata.Properties.ContainsKey(nameof ReferenceId) then\n                        returnValue.Properties.Add(nameof ReferenceId, Guid.Parse(branchEvent.Metadata.Properties[nameof ReferenceId]))\n\n                    // If there are child branch results, add them to the return properties.\n                    if branchEvent.Metadata.Properties.ContainsKey(\"ChildBranchResults\") then\n                        returnValue.Properties.Add(\"ChildBranchResults\", branchEvent.Metadata.Properties[\"ChildBranchResults\"])\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    let graceError = GraceError.CreateWithException ex (getErrorMessage BranchError.FailedWhileApplyingEvent) branchEvent.Metadata.CorrelationId\n\n                    graceError\n                        .enhance(nameof RepositoryId, branchDto.RepositoryId)\n                        .enhance(nameof BranchId, branchDto.BranchId)\n                        .enhance(nameof BranchName, branchDto.BranchName)\n                        .enhance(nameof ParentBranchId, branchDto.ParentBranchId)\n                        .enhance (nameof BranchEventType, getDiscriminatedUnionFullName branchEvent.Event)\n                    |> ignore\n\n                    // If the event has a referenceId, add it to the return properties.\n                    if branchEvent.Metadata.Properties.ContainsKey(nameof ReferenceId) then\n                        graceError.enhance (nameof ReferenceId, branchEvent.Metadata.Properties[nameof ReferenceId])\n                        |> ignore\n\n                    return Error graceError\n            }\n\n        interface IGraceReminderWithGuidKey with\n            /// Schedules a Grace reminder.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminder =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            branchDto.OwnerId\n                            branchDto.OrganizationId\n                            branchDto.RepositoryId\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminder\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                this.correlationId <- reminder.CorrelationId\n\n                task {\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.PhysicalDeletion, ReminderState.BranchPhysicalDeletion physicalDeletionReminderState ->\n                        this.correlationId <- physicalDeletionReminderState.CorrelationId\n\n                        // Delete saved state for this actor.\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Deleted physical state for branch; RepositoryId: {repositoryId}; BranchId: {branchId}; BranchName: {branchName}; ParentBranchId: {parentBranchId}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            physicalDeletionReminderState.CorrelationId,\n                            physicalDeletionReminderState.RepositoryId,\n                            physicalDeletionReminderState.BranchId,\n                            physicalDeletionReminderState.BranchName,\n                            physicalDeletionReminderState.ParentBranchId,\n                            physicalDeletionReminderState.DeleteReason\n                        )\n\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId\n                            )\n                }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = branchDto.RepositoryId |> returnTask\n\n        interface IBranchActor with\n\n            member this.GetEvents correlationId =\n                task {\n                    this.correlationId <- correlationId\n                    return state.State :> IReadOnlyList<BranchEvent>\n                }\n\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n                branchDto.UpdatedAt.IsSome |> returnTask\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                branchDto.DeletedAt.IsSome |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (command: BranchCommand) (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId)\n                           && (state.State.Count > 3) then\n                            return Error(GraceError.Create (getErrorMessage BranchError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | BranchCommand.Create (branchId, branchName, parentBranchId, basedOn, ownerId, organizationId, repositoryId, branchPermissions) ->\n                                match branchDto.UpdatedAt with\n                                | Some _ -> return Error(GraceError.Create (BranchError.getErrorMessage BranchAlreadyExists) metadata.CorrelationId)\n                                | None -> return Ok command\n                            | _ ->\n                                match branchDto.UpdatedAt with\n                                | Some _ -> return Ok command\n                                | None -> return Error(GraceError.Create (getErrorMessage BranchError.BranchDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let addReference ownerId organizationId repositoryId branchId directoryId sha256Hash referenceText referenceType links =\n                    task {\n                        let referenceId: ReferenceId = ReferenceId.NewGuid()\n                        let referenceActor = Reference.CreateActorProxy referenceId repositoryId this.correlationId\n\n                        let referenceCommand =\n                            ReferenceCommand.Create(\n                                referenceId,\n                                ownerId,\n                                organizationId,\n                                repositoryId,\n                                branchId,\n                                directoryId,\n                                sha256Hash,\n                                referenceType,\n                                referenceText,\n                                links\n                            )\n\n                        metadata.Properties[ nameof (RepositoryId) ] <- $\"{repositoryId}\"\n                        return! referenceActor.Handle referenceCommand metadata\n                    }\n\n                let addReferenceToCurrentBranch = addReference branchDto.OwnerId branchDto.OrganizationId branchDto.RepositoryId branchDto.BranchId\n\n                let processCommand (command: BranchCommand) (metadata: EventMetadata) =\n                    task {\n                        try\n                            //logToConsole\n                            //    $\"In BranchActor.Handle.processCommand: command: {getDiscriminatedUnionFullName command}; metadata: {serialize metadata}.\"\n\n                            let! event =\n                                task {\n                                    match command with\n                                    | Create (branchId, branchName, parentBranchId, basedOn, ownerId, organizationId, repositoryId, branchPermissions) ->\n                                        // Add an initial Rebase reference to this branch that points to the BasedOn reference, unless we're creating `main`.\n                                        if branchName <> InitialBranchName then\n                                            // We need to get the reference that we're rebasing on, so we can get the DirectoryId and Sha256Hash.\n                                            let referenceActorProxy = Reference.CreateActorProxy basedOn repositoryId this.correlationId\n                                            let! promotionDto = referenceActorProxy.Get this.correlationId\n\n                                            match!\n                                                addReference\n                                                    branchDto.OwnerId\n                                                    branchDto.OrganizationId\n                                                    repositoryId\n                                                    branchId\n                                                    promotionDto.DirectoryId\n                                                    promotionDto.Sha256Hash\n                                                    promotionDto.ReferenceText\n                                                    ReferenceType.Rebase\n                                                    [\n                                                        ReferenceLinkType.BasedOn promotionDto.ReferenceId\n                                                    ]\n                                                with\n                                            | Ok _ ->\n                                                //logToConsole $\"In BranchActor.Handle.processCommand: rebaseReferenceDto: {rebaseReferenceDto}.\"\n                                                ()\n                                            | Error error ->\n                                                logToConsole\n                                                    $\"In BranchActor.Handle.processCommand: Error rebasing on referenceId: {basedOn}. promotionDto: {serialize promotionDto}\"\n\n                                        memoryCache.CreateBranchNameEntry(repositoryId, branchName, branchId)\n\n                                        return\n                                            Ok(Created(branchId, branchName, parentBranchId, basedOn, ownerId, organizationId, repositoryId, branchPermissions))\n                                    | BranchCommand.Rebase referenceId ->\n                                        metadata.Properties[ \"BasedOn\" ] <- $\"{referenceId}\"\n                                        metadata.Properties[ nameof ReferenceId ] <- $\"{referenceId}\"\n                                        metadata.Properties[ nameof RepositoryId ] <- $\"{branchDto.RepositoryId}\"\n                                        metadata.Properties[ nameof BranchId ] <- $\"{this.GetGrainId().GetGuidKey()}\"\n                                        metadata.Properties[ nameof BranchName ] <- $\"{branchDto.BranchName}\"\n\n                                        // We need to get the reference that we're rebasing on, so we can get the directoryId and sha256Hash.\n                                        let referenceActorProxy = Reference.CreateActorProxy referenceId branchDto.RepositoryId this.correlationId\n                                        let! promotionDto = referenceActorProxy.Get metadata.CorrelationId\n\n                                        // Add the Rebase reference to this branch.\n                                        match!\n                                            addReferenceToCurrentBranch\n                                                promotionDto.DirectoryId\n                                                promotionDto.Sha256Hash\n                                                promotionDto.ReferenceText\n                                                ReferenceType.Rebase\n                                                [\n                                                    ReferenceLinkType.BasedOn promotionDto.ReferenceId\n                                                ]\n                                            with\n                                        | Ok rebaseReferenceDto ->\n                                            //logToConsole $\"In BranchActor.Handle.processCommand: rebaseReferenceDto: {rebaseReferenceDto}.\"\n                                            return Ok(Rebased referenceId)\n                                        | Error error ->\n                                            log.LogError(\n                                                \"{CurrentInstant}: Error rebasing on referenceId: {referenceId}; promotionDto: {promotionDto}.\\n{Error}\",\n                                                getCurrentInstantExtended (),\n                                                referenceId,\n                                                serialize promotionDto,\n                                                error\n                                            )\n\n                                            return Error error\n                                    | SetName branchName -> return Ok(NameSet branchName)\n                                    | EnableAssign enabled -> return Ok(EnabledAssign enabled)\n                                    | EnablePromotion enabled -> return Ok(EnabledPromotion enabled)\n                                    | EnableCommit enabled -> return Ok(EnabledCommit enabled)\n                                    | EnableCheckpoint enabled -> return Ok(EnabledCheckpoint enabled)\n                                    | EnableSave enabled -> return Ok(EnabledSave enabled)\n                                    | EnableTag enabled -> return Ok(EnabledTag enabled)\n                                    | EnableExternal enabled -> return Ok(EnabledExternal enabled)\n                                    | EnableAutoRebase enabled -> return Ok(EnabledAutoRebase enabled)\n                                    | SetPromotionMode promotionMode -> return Ok(PromotionModeSet promotionMode)\n                                    | UpdateParentBranch newParentBranchId -> return Ok(ParentBranchUpdated newParentBranchId)\n                                    | BranchCommand.Assign (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.Promotion List.empty with\n                                        | Ok returnValue -> return Ok(Assigned(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | BranchCommand.Promote (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.Promotion List.empty with\n                                        | Ok returnValue -> return Ok(Promoted(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | BranchCommand.Commit (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.Commit List.empty with\n                                        | Ok returnValue -> return Ok(Committed(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | BranchCommand.Checkpoint (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.Checkpoint List.empty with\n                                        | Ok returnValue -> return Ok(Checkpointed(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | BranchCommand.Save (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.Save List.empty with\n                                        | Ok returnValue -> return Ok(Saved(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | BranchCommand.Tag (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.Tag List.empty with\n                                        | Ok returnValue -> return Ok(Tagged(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | BranchCommand.CreateExternal (directoryVersionId, sha256Hash, referenceText) ->\n                                        match! addReferenceToCurrentBranch directoryVersionId sha256Hash referenceText ReferenceType.External List.empty with\n                                        | Ok returnValue -> return Ok(ExternalCreated(returnValue.ReturnValue, directoryVersionId, sha256Hash, referenceText))\n                                        | Error error -> return Error error\n                                    | RemoveReference referenceId -> return Ok(ReferenceRemoved referenceId)\n                                    | DeleteLogical (force, deleteReason, reassignChildBranches, newParentBranchId) ->\n                                        // Check for child branches\n                                        let! childBranches =\n                                            getChildBranches branchDto.RepositoryId branchDto.BranchId Int32.MaxValue false metadata.CorrelationId\n\n                                        if childBranches.Length > 0\n                                           && not reassignChildBranches\n                                           && not force then\n                                            // Cannot delete branch with children without reassigning or forcing deletion\n                                            return\n                                                Error(\n                                                    GraceError.Create\n                                                        (BranchError.getErrorMessage BranchError.CannotDeleteBranchesWithChildrenWithoutReassigningChildren)\n                                                        metadata.CorrelationId\n                                                )\n                                        else\n                                            // Track results for child branch operations\n                                            let childBranchResults = System.Collections.Concurrent.ConcurrentBag<string>()\n\n                                            // If force is set and there are child branches, delete them recursively\n                                            if force && childBranches.Length > 0 then\n                                                do!\n                                                    Parallel.ForEachAsync(\n                                                        childBranches,\n                                                        Constants.ParallelOptions,\n                                                        (fun childBranch ct ->\n                                                            ValueTask(\n                                                                task {\n                                                                    let childBranchActorProxy =\n                                                                        Branch.CreateActorProxy\n                                                                            childBranch.BranchId\n                                                                            branchDto.RepositoryId\n                                                                            metadata.CorrelationId\n\n                                                                    let childMetadata = EventMetadata.New metadata.CorrelationId GraceSystemUser\n\n                                                                    // Recursively delete child branch with force\n                                                                    match!\n                                                                        childBranchActorProxy.Handle\n                                                                            (DeleteLogical(\n                                                                                true,\n                                                                                $\"Parent branch {branchDto.BranchName} is being deleted.\",\n                                                                                false,\n                                                                                None\n                                                                            ))\n                                                                            childMetadata\n                                                                        with\n                                                                    | Ok _ -> childBranchResults.Add($\"Deleted child branch: {childBranch.BranchName}\")\n                                                                    | Error error ->\n                                                                        log.LogError(\n                                                                            \"{CurrentInstant}: Error deleting child branch {ChildBranchId}: {Error}\",\n                                                                            getCurrentInstantExtended (),\n                                                                            childBranch.BranchId,\n                                                                            error\n                                                                        )\n\n                                                                        childBranchResults.Add($\"Failed to delete child branch: {childBranch.BranchName}\")\n                                                                }\n                                                                :> Task\n                                                            ))\n                                                    )\n\n                                            // If reassigning children, determine the new parent and update them\n                                            if reassignChildBranches && childBranches.Length > 0 then\n                                                let targetParentBranchId =\n                                                    match newParentBranchId with\n                                                    | Some id -> id\n                                                    | None -> branchDto.ParentBranchId // Use the deleted branch's parent\n\n                                                // Reassign all child branches to the new parent\n                                                do!\n                                                    Parallel.ForEachAsync(\n                                                        childBranches,\n                                                        Constants.ParallelOptions,\n                                                        (fun childBranch ct ->\n                                                            ValueTask(\n                                                                task {\n                                                                    let childBranchActorProxy =\n                                                                        Branch.CreateActorProxy\n                                                                            childBranch.BranchId\n                                                                            branchDto.RepositoryId\n                                                                            metadata.CorrelationId\n\n                                                                    let childMetadata = EventMetadata.New metadata.CorrelationId GraceSystemUser\n\n                                                                    match! childBranchActorProxy.Handle (UpdateParentBranch targetParentBranchId) childMetadata\n                                                                        with\n                                                                    | Ok _ -> childBranchResults.Add($\"Reassigned child branch: {childBranch.BranchName}\")\n                                                                    | Error error ->\n                                                                        log.LogError(\n                                                                            \"{CurrentInstant}: Error updating parent branch for child {ChildBranchId}: {Error}\",\n                                                                            getCurrentInstantExtended (),\n                                                                            childBranch.BranchId,\n                                                                            error\n                                                                        )\n\n                                                                        childBranchResults.Add($\"Failed to reassign child branch: {childBranch.BranchName}\")\n                                                                }\n                                                                :> Task\n                                                            ))\n                                                    )\n\n                                            // Now proceed with the deletion regardless of reassignment\n                                            let tryGetLogicalDeleteDaysFromMetadata () =\n                                                match metadata.Properties.TryGetValue(\"RepositoryLogicalDeleteDays\") with\n                                                | true, value ->\n                                                    let mutable parsed = 0.0f\n\n                                                    if Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, &parsed) then\n                                                        Some parsed\n                                                    else\n                                                        None\n                                                | _ -> None\n\n                                            let! logicalDeleteDays =\n                                                match tryGetLogicalDeleteDaysFromMetadata () with\n                                                | Some days -> Task.FromResult days\n                                                | None ->\n                                                    task {\n                                                        let repositoryActorProxy =\n                                                            Repository.CreateActorProxy branchDto.OrganizationId branchDto.RepositoryId metadata.CorrelationId\n\n                                                        let! repositoryDto = repositoryActorProxy.Get(metadata.CorrelationId)\n                                                        return repositoryDto.LogicalDeleteDays\n                                                    }\n\n                                            // Delete the references for this branch.\n                                            let! references = getReferences branchDto.RepositoryId branchDto.BranchId Int32.MaxValue metadata.CorrelationId\n\n                                            do!\n                                                Parallel.ForEachAsync(\n                                                    references,\n                                                    Constants.ParallelOptions,\n                                                    (fun reference ct ->\n                                                        ValueTask(\n                                                            task {\n                                                                let referenceActorProxy =\n                                                                    Reference.CreateActorProxy\n                                                                        reference.ReferenceId\n                                                                        branchDto.RepositoryId\n                                                                        metadata.CorrelationId\n\n                                                                let metadata = EventMetadata.New metadata.CorrelationId GraceSystemUser\n                                                                metadata.Properties[ nameof (RepositoryId) ] <- $\"{branchDto.RepositoryId}\"\n\n                                                                metadata.Properties[ \"RepositoryLogicalDeleteDays\" ] <-\n                                                                    logicalDeleteDays.ToString(\"F\", CultureInfo.InvariantCulture)\n\n                                                                match!\n                                                                    referenceActorProxy.Handle\n                                                                        (ReferenceCommand.DeleteLogical(\n                                                                            true,\n                                                                            $\"Branch {branchDto.BranchName} is being deleted.\"\n                                                                        ))\n                                                                        metadata\n                                                                    with\n                                                                | Ok _ -> ()\n                                                                | Error error ->\n                                                                    log.LogError(\n                                                                        \"{CurrentInstant}: Error deleting reference {ReferenceId}: {Error}\",\n                                                                        getCurrentInstantExtended (),\n                                                                        reference.ReferenceId,\n                                                                        error\n                                                                    )\n\n                                                            }\n                                                            :> Task\n                                                        ))\n                                                )\n\n                                            let (physicalDeletionReminderState: PhysicalDeletionReminderState) =\n                                                {\n                                                    RepositoryId = branchDto.RepositoryId\n                                                    BranchId = branchDto.BranchId\n                                                    BranchName = branchDto.BranchName\n                                                    ParentBranchId = branchDto.ParentBranchId\n                                                    DeleteReason = deleteReason\n                                                    CorrelationId = metadata.CorrelationId\n                                                }\n\n                                            do!\n                                                (this :> IGraceReminderWithGuidKey)\n                                                    .ScheduleReminderAsync\n                                                    ReminderTypes.PhysicalDeletion\n                                                    (Duration.FromDays(float logicalDeleteDays))\n                                                    (ReminderState.BranchPhysicalDeletion physicalDeletionReminderState)\n                                                    metadata.CorrelationId\n\n                                            // Add child branch results to metadata for output\n                                            if childBranchResults.Count > 0 then\n                                                metadata.Properties[ \"ChildBranchResults\" ] <-\n                                                    childBranchResults.ToArray()\n                                                    |> String.concat Environment.NewLine\n\n                                            return Ok(LogicalDeleted(force, deleteReason, reassignChildBranches, newParentBranchId))\n                                    | DeletePhysical ->\n                                        // Delete the state from storage, and deactivate the actor.\n                                        do! state.ClearStateAsync()\n                                        this.DeactivateOnIdle()\n                                        return Ok PhysicalDeleted\n                                    | Undelete -> return Ok Undeleted\n                                }\n\n                            match event with\n                            | Ok event -> return! this.ApplyEvent { Event = event; Metadata = metadata }\n                            | Error error -> return Error error\n                        with\n                        | ex ->\n                            log.LogError(\n                                ex,\n                                \"{CurrentInstant}: In Branch.Actor.Handle.processCommand: Error processing command {Command}.\",\n                                getCurrentInstantExtended (),\n                                getDiscriminatedUnionFullName command\n                            )\n\n                            return Error(GraceError.CreateWithException ex String.Empty metadata.CorrelationId)\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n\n            member this.Get correlationId =\n                task {\n                    this.correlationId <- correlationId\n\n                    if branchDto.ShouldRecomputeLatestReferences then\n                        let! branchDtoWithLatestReferences = updateLatestReferences branchDto correlationId\n                        branchDto <- branchDtoWithLatestReferences\n\n                    return branchDto\n                }\n\n            member this.GetParentBranch correlationId =\n                task {\n                    this.correlationId <- correlationId\n                    let branchActorProxy = Branch.CreateActorProxy branchDto.ParentBranchId branchDto.RepositoryId correlationId\n\n                    return! branchActorProxy.Get correlationId\n                }\n\n            member this.GetLatestCommit correlationId =\n                this.correlationId <- correlationId\n                branchDto.LatestCommit |> returnTask\n\n            member this.GetLatestPromotion correlationId =\n                this.correlationId <- correlationId\n                branchDto.LatestPromotion |> returnTask\n\n            member this.MarkForRecompute(correlationId: CorrelationId) : Task =\n                this.correlationId <- correlationId\n                branchDto <- { branchDto with ShouldRecomputeLatestReferences = true }\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/BranchName.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\nopen OrganizationName\n\nmodule BranchName =\n\n    //let log = loggerFactory.CreateLogger(\"BranchName.Actor\")\n\n    type BranchNameActor() =\n        inherit Grain()\n\n        static let actorName = ActorName.BranchName\n\n        let log = loggerFactory.CreateLogger(\"BranchName.Actor\")\n\n        let mutable cachedBranchId: Guid option = None\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime \"In-memory only\"\n\n            Task.CompletedTask\n\n        interface IBranchNameActor with\n            member this.GetBranchId correlationId =\n                this.correlationId <- correlationId\n                Task.FromResult(cachedBranchId)\n\n            member this.SetBranchId branchId correlationId =\n                this.correlationId <- correlationId\n                cachedBranchId <- Some branchId\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/CodeGenAttribute.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen System.Runtime.CompilerServices\n\n[<assembly: InternalsVisibleTo(\"Grace.Orleans.CodeGen\")>]\n[<assembly: InternalsVisibleTo(\"Host\")>]\n\ndo ()\n"
  },
  {
    "path": "src/Grace.Actors/Constants.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen System\nopen System.Collections.Concurrent\n\nmodule Constants =\n\n    /// Constants for the names of the actors.\n    ///\n    /// These names should exactly match the actors' typenames.\n    module ActorName =\n        [<Literal>]\n        let AccessControl = \"AccessControlActor\"\n\n        [<Literal>]\n        let Branch = \"BranchActor\"\n\n        [<Literal>]\n        let BranchName = \"BranchNameActor\"\n\n        [<Literal>]\n        let Checkpoint = \"CheckpointActor\"\n\n        [<Literal>]\n        let Diff = \"DiffActor\"\n\n        [<Literal>]\n        let DirectoryVersion = \"DirectoryVersionActor\"\n\n        [<Literal>]\n        let DirectoryAppearance = \"DirectoryAppearanceActor\"\n\n        [<Literal>]\n        let FileAppearance = \"FileAppearanceActor\"\n\n        [<Literal>]\n        let GlobalLock = \"GlobalLockActor\"\n\n        [<Literal>]\n        let GrainRepository = \"GrainRepository\"\n\n        [<Literal>]\n        let Notification = \"NotificationActor\"\n\n        [<Literal>]\n        let Organization = \"OrganizationActor\"\n\n        [<Literal>]\n        let OrganizationName = \"OrganizationNameActor\"\n\n        [<Literal>]\n        let Owner = \"OwnerActor\"\n\n        [<Literal>]\n        let OwnerName = \"OwnerNameActor\"\n\n        [<Literal>]\n        let NamedSection = \"NamedSectionActor\"\n\n        [<Literal>]\n        let PersonalAccessToken = \"PersonalAccessTokenActor\"\n\n        [<Literal>]\n        let PromotionSet = \"PromotionSetActor\"\n\n        [<Literal>]\n        let PromotionQueue = \"PromotionQueueActor\"\n\n        [<Literal>]\n        let Policy = \"PolicyActor\"\n\n        [<Literal>]\n        let Review = \"ReviewActor\"\n\n        [<Literal>]\n        let ValidationSet = \"ValidationSetActor\"\n\n        [<Literal>]\n        let ValidationResult = \"ValidationResultActor\"\n\n        [<Literal>]\n        let Artifact = \"ArtifactActor\"\n\n        [<Literal>]\n        let Reference = \"ReferenceActor\"\n\n        [<Literal>]\n        let Reminder = \"ReminderActor\"\n\n        [<Literal>]\n        let Repository = \"RepositoryActor\"\n\n        [<Literal>]\n        let RepositoryName = \"RepositoryNameActor\"\n\n        [<Literal>]\n        let RepositoryPermission = \"RepositoryPermissionActor\"\n\n        [<Literal>]\n        let User = \"UserActor\"\n\n        [<Literal>]\n        let WorkItem = \"WorkItemActor\"\n\n        [<Literal>]\n        let WorkItemNumber = \"WorkItemNumberActor\"\n\n        [<Literal>]\n        let WorkItemNumberCounter = \"WorkItemNumberCounterActor\"\n\n    module StateName =\n        [<Literal>]\n        let AccessControl = \"AccessControl\"\n\n        [<Literal>]\n        let Branch = \"Branch\"\n\n        [<Literal>]\n        let Diff = \"Diff\"\n\n        [<Literal>]\n        let DirectoryAppearance = \"DirApp\"\n\n        [<Literal>]\n        let DirectoryVersion = \"Dir\"\n\n        [<Literal>]\n        let FileAppearance = \"FileApp\"\n\n        [<Literal>]\n        let NamedSection = \"NamedSection\"\n\n        [<Literal>]\n        let Organization = \"Organization\"\n\n        [<Literal>]\n        let Owner = \"Owner\"\n\n        [<Literal>]\n        let PersonalAccessToken = \"PersonalAccessToken\"\n\n        [<Literal>]\n        let PromotionSet = \"PromotionSet\"\n\n        [<Literal>]\n        let PromotionQueue = \"PromotionQueue\"\n\n        [<Literal>]\n        let Policy = \"Policy\"\n\n        [<Literal>]\n        let Review = \"Review\"\n\n        [<Literal>]\n        let ValidationSet = \"ValidationSet\"\n\n        [<Literal>]\n        let ValidationResult = \"ValidationResult\"\n\n        [<Literal>]\n        let Artifact = \"Artifact\"\n\n        [<Literal>]\n        let Reference = \"Ref\"\n\n        [<Literal>]\n        let Reminder = \"Rmd\"\n\n        [<Literal>]\n        let Repository = \"Repo\"\n\n        [<Literal>]\n        let RepositoryPermission = \"RepoPermission\"\n\n        [<Literal>]\n        let User = \"User\"\n\n        [<Literal>]\n        let WorkItem = \"WorkItem\"\n\n        [<Literal>]\n        let WorkItemNumberCounter = \"WorkItemNumberCounter\"\n\n    /// Constants for the different types of reminders.\n    module ReminderType =\n        [<Literal>]\n        let Maintenance = \"Maintenance\"\n\n        [<Literal>]\n        let PhysicalDeletion = \"PhysicalDeletion\"\n\n        [<Literal>]\n        let DeleteCachedState = \"DeleteCachedState\"\n\n    module LockName =\n        [<Literal>]\n        let ReminderLock = \"ReminderLock\"\n\n    let DefaultObjectStorageContainerName = \"grace-objects\"\n\n    /// The time to wait between logical and physical deletion of an actor's state.\n    ///\n    /// In Release builds, this is TimeSpan.FromDays(7.0). In Debug builds, it's TimeSpan.FromSeconds(300.0).\n#if DEBUG\n    let DefaultPhysicalDeletionReminderDuration = Duration.FromSeconds(300.0)\n#else\n    let DefaultPhysicalDeletionReminderDuration = Duration.FromDays(7.0)\n#endif\n\n    /// The time to wait between logical and physical deletion of an actor's state, as a TimeSpan.\n    let DefaultPhysicalDeletionReminderTimeSpan = DefaultPhysicalDeletionReminderDuration.ToTimeSpan()\n"
  },
  {
    "path": "src/Grace.Actors/Context.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Azure.Core\nopen Azure.Identity\nopen Azure.Storage.Blobs\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.Extensions.ObjectPool\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen System\nopen System.Text\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen Azure.Storage.Blobs.Models\nopen NodaTime.Serialization.SystemTextJson\nopen MessagePack\nopen MessagePack.Resolvers\nopen MessagePack.FSharp\nopen MessagePack.NodaTime\nopen MessagePack.Resolvers\n\nopen Grace.Shared.AzureEnvironment\n\nmodule Context =\n\n    /// Actor state storage provider instance\n    let mutable internal actorStateStorageProvider = ActorStateStorageProvider.Unknown\n\n    /// Setter for actor state storage provider\n    let setActorStateStorageProvider storageProvider =\n        logToConsole $\"In Context.Actor.setActorStateStorageProvider: Setting actor state storage provider to {storageProvider}.\"\n        actorStateStorageProvider <- storageProvider\n\n    /// Orleans client instance for the application.\n    let mutable internal orleansClient: IGrainFactory = null\n\n    /// Sets the Orleans client for the application.\n    let setOrleansClient (client: IGrainFactory) = orleansClient <- client\n\n    /// Cosmos client instance\n    let mutable internal cosmosClient: CosmosClient = null\n\n    /// Setter for Cosmos client\n    let setCosmosClient (client: CosmosClient) = cosmosClient <- client\n\n    /// Cosmos container instance\n    let mutable internal cosmosContainer: Container = null\n\n    /// Setter for Cosmos container\n    let setCosmosContainer (container: Container) = cosmosContainer <- container\n\n    /// Host services collection\n    let mutable internal hostServiceProvider: IServiceProvider = null\n\n    /// Setter for services collection\n    let setHostServiceProvider (hostServices: IServiceProvider) = hostServiceProvider <- hostServices\n\n    /// Logger factory instance\n    let mutable internal loggerFactory: ILoggerFactory = null //hostServiceProvider.GetService(typeof<ILoggerFactory>) :?> ILoggerFactory\n\n    /// Setter for logger factory\n    let setLoggerFactory (factory: ILoggerFactory) = loggerFactory <- factory\n\n    /// Pub-sub settings for Grace.\n    let mutable internal pubSubSettings: GracePubSubSettings = GracePubSubSettings.Empty\n    let setPubSubSettings (settings: GracePubSubSettings) = pubSubSettings <- settings\n\n    let mutable internal timings = ConcurrentDictionary<CorrelationId, List<Timing>>()\n    let setTimings (timing: ConcurrentDictionary<CorrelationId, List<Timing>>) = timings <- timing\n\n    let private defaultAzureCredential = lazy (DefaultAzureCredential())\n\n    /// Azure Blob Storage client\n    let blobServiceClient =\n        if AzureEnvironment.useManagedIdentityForStorage then\n            BlobServiceClient(AzureEnvironment.storageEndpoints.BlobEndpoint, defaultAzureCredential.Value)\n        else\n            match AzureEnvironment.storageEndpoints.ConnectionString with\n            | Some connectionString -> BlobServiceClient(connectionString)\n            | None -> invalidOp \"Azure Storage connection string must be configured when running in local debug mode without a managed identity.\"\n"
  },
  {
    "path": "src/Grace.Actors/Diff.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Specialized\nopen DiffPlex\nopen DiffPlex.DiffBuilder.Model\nopen FSharpPlus\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Diff\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.Diff\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Linq\nopen System.IO\nopen System.IO.Compression\nopen System.Threading.Tasks\nopen Grace.Actors.Extensions\n\nmodule Diff =\n\n    type DiffActor([<PersistentState(StateName.Diff, Constants.GraceDiffStorage)>] state: IPersistentState<DiffDto>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Diff\n\n        let log = loggerFactory.CreateLogger(\"Diff.Actor\")\n\n        let mutable diffDto: DiffDto = DiffDto.Default\n\n        /// Gets a Dictionary for indexed lookups by relative path.\n        let getLookupCache (graceIndex: ServerGraceIndex) =\n            let lookupCache = Dictionary<FileSystemEntryType * RelativePath, Sha256Hash>()\n\n            for directoryVersion in graceIndex.Values do\n                // Add the directory to the lookup cache.\n                lookupCache.TryAdd((FileSystemEntryType.Directory, directoryVersion.RelativePath), directoryVersion.Sha256Hash)\n                |> ignore\n                // Add each file to the lookup cache.\n                for file in directoryVersion.Files do\n                    lookupCache.TryAdd((FileSystemEntryType.File, file.RelativePath), file.Sha256Hash)\n                    |> ignore\n\n            lookupCache\n\n        /// Scans two ServerGraceIndex instances for differences.\n        let scanForDifferences (newerGraceIndex: ServerGraceIndex) (olderGraceIndex: ServerGraceIndex) =\n            task {\n                let emptyLookup = KeyValuePair(String.Empty, Sha256Hash String.Empty)\n                let differences = List<FileSystemDifference>()\n\n                // Create an indexed lookup table of path -> lastWriteTimeUtc from the Grace index file.\n                let olderLookupCache = getLookupCache olderGraceIndex\n                let newerLookupCache = getLookupCache newerGraceIndex\n\n                // Compare them for differences.\n                for kvp in olderLookupCache do\n                    let ((fileSystemEntryType, relativePath), sha256Hash) = kvp.Deconstruct()\n                    // Find the entries that changed\n                    if\n                        newerLookupCache.ContainsKey((fileSystemEntryType, relativePath))\n                        && sha256Hash\n                           <> newerLookupCache.Item((fileSystemEntryType, relativePath))\n                    then\n                        differences.Add(FileSystemDifference.Create Change fileSystemEntryType relativePath)\n\n                    // Find the entries that were deleted\n                    elif\n                        not\n                        <| newerLookupCache.ContainsKey((fileSystemEntryType, relativePath))\n                    then\n                        differences.Add(FileSystemDifference.Create Delete fileSystemEntryType relativePath)\n\n                // Find the entries that were added\n                for kvp in newerLookupCache do\n                    let ((fileSystemEntryType, relativePath), sha256Hash) = kvp.Deconstruct()\n\n                    if\n                        not\n                        <| olderLookupCache.ContainsKey((fileSystemEntryType, relativePath))\n                    then\n                        differences.Add(FileSystemDifference.Create Add fileSystemEntryType relativePath)\n\n                return differences\n            }\n\n        /// Deconstructs an ActorId of the form \"{directoryVersionId1}*{directoryVersionId2}\" into a tuple of the two DirectoryId values.\n        let deconstructActorId (primaryKey: string) =\n            let directoryIds = primaryKey.Split(\"*\")\n            (DirectoryVersionId directoryIds[0], DirectoryVersionId directoryIds[1])\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        /// Builds a ServerGraceIndex from a root DirectoryId.\n        member private this.buildGraceIndex (directoryId: DirectoryVersionId) repositoryId correlationId =\n            task {\n                this.correlationId <- correlationId\n                let graceIndex = ServerGraceIndex()\n\n                let directoryVersionActorProxy = ActorProxy.DirectoryVersion.CreateActorProxy directoryId repositoryId correlationId\n\n                let! directoryCreatedAt = directoryVersionActorProxy.GetCreatedAt correlationId\n                let! directoryVersionDtos = directoryVersionActorProxy.GetRecursiveDirectoryVersions false correlationId\n\n                for directoryVersionDto in directoryVersionDtos do\n                    let directoryVersion = directoryVersionDto.DirectoryVersion\n\n                    graceIndex.TryAdd(directoryVersion.RelativePath, directoryVersion)\n                    |> ignore\n\n                return (graceIndex, directoryCreatedAt)\n            }\n\n        /// Gets a Stream from object storage for a specific FileVersion, using a generated Uri.\n        member private this.getUncompressedStream (repositoryDto: RepositoryDto) (fileVersion: FileVersion) (url: UriWithSharedAccessSignature) correlationId =\n            task {\n                this.correlationId <- correlationId\n                let objectStorageProvider = repositoryDto.ObjectStorageProvider\n\n                match objectStorageProvider with\n                | AWSS3 -> return new MemoryStream() :> Stream\n                | AzureBlobStorage ->\n                    let blobClient = BlockBlobClient(url)\n                    let! fileStream = blobClient.OpenReadAsync(position = 0, bufferSize = (64 * 1024))\n\n                    let uncompressedStream =\n                        if fileVersion.IsBinary then\n                            fileStream\n                        else\n                            let gzStream = new GZipStream(stream = fileStream, mode = CompressionMode.Decompress, leaveOpen = false)\n                            gzStream :> Stream\n\n                    return uncompressedStream\n                | GoogleCloudStorage -> return new MemoryStream() :> Stream\n                | ObjectStorageProvider.Unknown -> return new MemoryStream() :> Stream\n            }\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n            if state.RecordExists then diffDto <- state.State\n            Task.CompletedTask\n\n        interface IDiffActor with\n            /// Sets a Grace reminder to perform a physical deletion of this actor.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminder =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            diffDto.OwnerId\n                            diffDto.OrganizationId\n                            diffDto.RepositoryId\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminder\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    this.correlationId <- reminder.CorrelationId\n\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.DeleteCachedState, ReminderState.DiffDeleteCachedState deleteCachedStateReminderState ->\n                        this.correlationId <- deleteCachedStateReminderState.CorrelationId\n\n                        // Delete saved state for this actor.\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Deleted cache for diff; RepositoryId: {RepositoryId}; DirectoryVersionId1: {DirectoryVersionId1}; DirectoryVersionId2: {DirectoryVersionId2}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            deleteCachedStateReminderState.CorrelationId,\n                            diffDto.RepositoryId,\n                            diffDto.DirectoryVersionId1,\n                            diffDto.DirectoryVersionId2,\n                            deleteCachedStateReminderState.DeleteReason\n                        )\n\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                (GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId)\n                                    .enhance (\"IsRetryable\", \"false\")\n                            )\n                }\n\n            member this.Compute correlationId : Task<GraceResult<string>> =\n                this.correlationId <- correlationId\n\n                task {\n                    try\n\n                        // If it's already populated, skip this.\n                        if diffDto.DirectoryVersionId1\n                           <> DiffDto.Default.DirectoryVersionId1 then\n                            return\n                                Ok(\n                                    (GraceReturnValue.Create<string> \"DiffActor.Compute: already populated.\" correlationId)\n                                        .enhance(\"DirectoryVersionId1\", $\"{diffDto.DirectoryVersionId1}\")\n                                        .enhance(\"DirectoryVersionId2\", $\"{diffDto.DirectoryVersionId2}\")\n                                        .enhance(\"OwnerId\", $\"{diffDto.OwnerId}\")\n                                        .enhance(\"OrganizationId\", $\"{diffDto.OrganizationId}\")\n                                        .enhance(\"RepositoryId\", $\"{diffDto.RepositoryId}\")\n                                        .enhance (\"HasDifferences\", $\"{diffDto.HasDifferences}\")\n                                )\n                        else\n                            let (directoryVersionId1, directoryVersionId2) = deconstructActorId ($\"{this.GetGrainId().Key}\")\n\n                            //logToConsole $\"In DiffActor.Populate(); DirectoryVersionId1: {directoryVersionId1}; DirectoryVersionId2: {directoryVersionId2}\"\n\n                            let orleansContext = memoryCache.GetOrleansContextEntry(this.GetGrainId())\n                            let ownerId = orleansContext.Value[nameof OwnerId] :?> OwnerId\n                            let organizationId = orleansContext.Value[nameof OrganizationId] :?> OrganizationId\n                            let repositoryId = orleansContext.Value[nameof RepositoryId] :?> RepositoryId\n                            let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryId correlationId\n                            let! repositoryDto = repositoryActorProxy.Get correlationId\n\n                            // Build a GraceIndex for each DirectoryId.\n                            let! (graceIndex1, createdAt1) = this.buildGraceIndex directoryVersionId1 repositoryId correlationId\n\n                            let! (graceIndex2, createdAt2) = this.buildGraceIndex directoryVersionId2 repositoryId correlationId\n\n                            //logToConsole $\"In DiffActor.Populate(); createdAt1: {createdAt1}; createdAt2: {createdAt2}.\"\n\n                            // Compare the GraceIndices.\n                            let! differences =\n                                task {\n                                    if createdAt1.CompareTo(createdAt2) > 0 then\n                                        return! scanForDifferences graceIndex1 graceIndex2\n                                    else\n                                        return! scanForDifferences graceIndex2 graceIndex1\n                                }\n\n                            //logToConsole $\"In Actor.Populate(); got differences.\"\n\n                            diffDto <- { diffDto with OwnerId = ownerId; OrganizationId = organizationId; RepositoryId = repositoryDto.RepositoryId }\n\n                            /// Gets a Stream for a given RelativePath.\n                            let getFileStream (graceIndex: ServerGraceIndex) (relativePath: RelativePath) (repositoryDto: RepositoryDto) =\n                                task {\n                                    let relativeDirectoryPath = getRelativeDirectory relativePath Constants.RootDirectoryPath\n                                    //logToConsole $\"In DiffActor.getFileStream(); relativePath: {relativePath}; relativeDirectoryPath: {relativeDirectoryPath}; graceIndex.Count: {graceIndex.Count}.\"\n                                    let directory = graceIndex[relativeDirectoryPath]\n                                    let fileVersion = directory.Files.First(fun f -> f.RelativePath = relativePath)\n\n                                    let! uri = getUriWithReadSharedAccessSignatureForFileVersion repositoryDto fileVersion correlationId\n                                    let! uncompressedStream = this.getUncompressedStream repositoryDto fileVersion uri correlationId\n                                    return Ok(uncompressedStream, fileVersion)\n                                }\n\n                            // Process each difference.\n                            let fileDiffs = ConcurrentBag<FileDiff>()\n\n                            do!\n                                Parallel.ForEachAsync(\n                                    differences,\n                                    Constants.ParallelOptions,\n                                    (fun difference ct ->\n                                        ValueTask(\n                                            task {\n                                                match difference.DifferenceType with\n                                                | Change ->\n                                                    // This is the only case that we need to generate file diffs for.\n                                                    match difference.FileSystemEntryType with\n                                                    | Directory -> () // Might have to revisit this.\n                                                    | File ->\n                                                        // Get streams for both file versions.\n                                                        let! result1 = getFileStream graceIndex1 difference.RelativePath repositoryDto\n\n                                                        let! result2 = getFileStream graceIndex2 difference.RelativePath repositoryDto\n\n                                                        match (result1, result2) with\n                                                        | (Ok (fileStream1, fileVersion1), Ok (fileStream2, fileVersion2)) ->\n                                                            try\n                                                                // Compare the streams using DiffPlex, and get the Inline and Side-by-Side diffs.\n                                                                let! diffResults =\n                                                                    task {\n                                                                        if createdAt1.CompareTo(createdAt2) < 0 then\n                                                                            return! diffTwoFiles fileStream1 fileStream2\n                                                                        else\n                                                                            return! diffTwoFiles fileStream2 fileStream1\n                                                                    }\n\n                                                                // Create a FileDiff with the DiffPlex results and corresponding Sha256Hash values.\n                                                                let fileDiff =\n                                                                    if createdAt1.CompareTo(createdAt2) < 0 then\n                                                                        FileDiff.Create\n                                                                            fileVersion1.RelativePath\n                                                                            fileVersion1.Sha256Hash\n                                                                            fileVersion1.CreatedAt\n                                                                            fileVersion2.Sha256Hash\n                                                                            fileVersion2.CreatedAt\n                                                                            (fileVersion1.IsBinary || fileVersion2.IsBinary)\n                                                                            diffResults.InlineDiff\n                                                                            diffResults.SideBySideOld\n                                                                            diffResults.SideBySideNew\n                                                                    else\n                                                                        FileDiff.Create\n                                                                            fileVersion1.RelativePath\n                                                                            fileVersion2.Sha256Hash\n                                                                            fileVersion1.CreatedAt\n                                                                            fileVersion1.Sha256Hash\n                                                                            fileVersion2.CreatedAt\n                                                                            (fileVersion1.IsBinary || fileVersion2.IsBinary)\n                                                                            diffResults.InlineDiff\n                                                                            diffResults.SideBySideOld\n                                                                            diffResults.SideBySideNew\n\n                                                                fileDiffs.Add(fileDiff)\n                                                            finally\n                                                                if not <| isNull fileStream1 then fileStream1.Dispose()\n                                                                if not <| isNull fileStream2 then fileStream2.Dispose()\n                                                        | (Error ex, _) -> raise ex\n                                                        | (_, Error ex) -> raise ex\n                                                | Add -> ()\n                                                | Delete -> ()\n                                            }\n                                        ))\n                                )\n\n                            diffDto.FileDiffs.AddRange(fileDiffs.ToArray())\n\n                            diffDto <-\n                                { diffDto with\n                                    HasDifferences = differences.Count <> 0\n                                    RepositoryId = repositoryDto.RepositoryId\n                                    DirectoryVersionId1 = directoryVersionId1\n                                    Directory1CreatedAt = createdAt1\n                                    DirectoryVersionId2 = directoryVersionId2\n                                    Directory2CreatedAt = createdAt2\n                                    Differences = differences\n                                }\n\n                            state.State <- diffDto\n                            do! state.WriteStateAsync()\n\n                            let (deleteCachedStateReminderState: DeleteCachedStateReminderState) =\n                                { DeleteReason = getDiscriminatedUnionCaseName ReminderTypes.DeleteCachedState; CorrelationId = correlationId }\n\n                            do!\n                                (this :> IDiffActor).ScheduleReminderAsync\n                                    ReminderTypes.DeleteCachedState\n                                    (Duration.FromDays(float repositoryDto.DiffCacheDays))\n                                    (ReminderState.DiffDeleteCachedState deleteCachedStateReminderState)\n                                    correlationId\n\n                            return\n                                Ok(\n                                    (GraceReturnValue.Create<string> \"DiffActor.Compute: populated.\" correlationId)\n                                        .enhance(\"DirectoryVersionId1\", $\"{diffDto.DirectoryVersionId1}\")\n                                        .enhance(\"DirectoryVersionId2\", $\"{diffDto.DirectoryVersionId2}\")\n                                        .enhance(\"OwnerId\", $\"{diffDto.OwnerId}\")\n                                        .enhance(\"OrganizationId\", $\"{diffDto.OrganizationId}\")\n                                        .enhance(\"RepositoryId\", $\"{diffDto.RepositoryId}\")\n                                        .enhance (\"HasDifferences\", $\"{diffDto.HasDifferences}\")\n                                )\n                    with\n                    | ex ->\n                        logToConsole $\"Exception in DiffActor.Compute(): {ExceptionResponse.Create ex}\"\n                        logToConsole $\"directoryVersionId1: {diffDto.DirectoryVersionId1}; directoryVersionId2: {diffDto.DirectoryVersionId2}\"\n\n                        if not <| isNull Activity.Current then\n                            Activity\n                                .Current\n                                .SetStatus(ActivityStatusCode.Error, \"Exception while creating diff.\")\n                                .AddTag(\"ex.Message\", ex.Message)\n                                .AddTag(\"ex.StackTrace\", ex.StackTrace)\n\n                                .AddTag(\n                                    \"directoryVersionId1\",\n                                    $\"{diffDto.DirectoryVersionId1}\"\n                                )\n                                .AddTag(\"directoryVersionId2\", $\"{diffDto.DirectoryVersionId2}\")\n                            |> ignore\n\n                        return\n                            Error(\n                                (GraceError.Create \"Exception while creating diff.\" correlationId)\n                                    .enhance(\"DirectoryVersionId1\", $\"{diffDto.DirectoryVersionId1}\")\n                                    .enhance(\"DirectoryVersionId2\", $\"{diffDto.DirectoryVersionId2}\")\n                                    .enhance(\"OwnerId\", $\"{diffDto.OwnerId}\")\n                                    .enhance(\"OrganizationId\", $\"{diffDto.OrganizationId}\")\n                                    .enhance(\"RepositoryId\", $\"{diffDto.RepositoryId}\")\n                                    .enhance (\"HasDifferences\", $\"{diffDto.HasDifferences}\")\n                            )\n                }\n\n            member this.GetDiff correlationId =\n                task {\n                    this.correlationId <- correlationId\n\n                    if diffDto.DirectoryVersionId1.Equals(DiffDto.Default.DirectoryVersionId1) then\n                        let! populated = (this :> IDiffActor).Compute correlationId\n\n                        log.LogTrace(\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DiffActor.GetDiff(); was not previously computed.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            correlationId,\n                            populated\n                        )\n                    else\n                        logToConsole $\"In Actor.GetDiff(), already populated.\"\n\n                    return diffDto\n                }\n"
  },
  {
    "path": "src/Grace.Actors/DirectoryAppearance.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule DirectoryAppearance =\n\n    type DirectoryAppearanceDto() =\n        member val public Appearances = SortedSet<Appearance>() with get, set\n        member val public RepositoryId: RepositoryId = RepositoryId.Empty with get, set\n\n    type DirectoryAppearanceActor\n        (\n            [<PersistentState(StateName.DirectoryAppearance, Constants.GraceActorStorage)>] state: IPersistentState<SortedSet<Appearance>>\n        ) =\n        inherit Grain()\n\n        let actorName = ActorName.DirectoryAppearance\n\n        let log = loggerFactory.CreateLogger(\"DirectoryAppearance.Actor\")\n\n        let directoryAppearanceDto = DirectoryAppearanceDto()\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            directoryAppearanceDto.Appearances <- state.State\n            Task.CompletedTask\n\n        interface IDirectoryAppearanceActor with\n\n            member this.Add appearance correlationId =\n                task {\n                    let wasAdded = directoryAppearanceDto.Appearances.Add(appearance)\n\n                    if wasAdded then do! state.WriteStateAsync()\n                }\n                :> Task\n\n            member this.Remove appearance correlationId =\n                task {\n                    let wasRemoved = directoryAppearanceDto.Appearances.Remove(appearance)\n\n                    if wasRemoved then\n                        if directoryAppearanceDto.Appearances |> Seq.isEmpty then\n                            do! state.ClearStateAsync()\n\n                            let directoryVersionGuid = this.GetGrainId().GetGuidKey()\n\n                            let directoryVersionActorProxy =\n                                DirectoryVersion.CreateActorProxy directoryVersionGuid directoryAppearanceDto.RepositoryId correlationId\n\n                            let! result = directoryVersionActorProxy.Delete(correlationId)\n\n                            match result with\n                            | Ok returnValue -> ()\n                            | Error error -> ()\n\n                            ()\n                        else\n                            do! state.WriteStateAsync()\n                }\n                :> Task\n\n            member this.Contains appearance correlationId =\n                directoryAppearanceDto.Appearances.Contains(appearance)\n                |> returnTask\n\n            member this.Appearances correlationId = directoryAppearanceDto.Appearances |> returnTask\n"
  },
  {
    "path": "src/Grace.Actors/DirectoryVersion.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Models\nopen Azure.Storage.Blobs.Specialized\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Services\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Events\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.ObjectPool\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Buffers\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.IO.Compression\nopen System.Linq\nopen System.Security.Cryptography\nopen System.Text\nopen System.Threading.Tasks\nopen System.Reflection.Metadata\nopen MessagePack\nopen System.Threading\nopen Azure.Storage\n\nmodule DirectoryVersion =\n\n    /// Result of validating a single file's SHA-256 hash.\n    type FileValidationResult =\n        | Valid of fileVersion: FileVersion * computedHash: Sha256Hash * elapsedMs: float\n        | HashMismatch of fileVersion: FileVersion * expectedHash: Sha256Hash * computedHash: Sha256Hash * elapsedMs: float\n        | MissingInStorage of fileVersion: FileVersion * elapsedMs: float\n        | ValidationError of fileVersion: FileVersion * errorMessage: string * elapsedMs: float\n\n    /// Validates a single file's SHA-256 hash by downloading from storage and computing.\n    /// Note: Non-binary files are stored as GZip-compressed streams, so we need to decompress them first.\n    let validateFileSha256 (repositoryDto: RepositoryDto) (fileVersion: FileVersion) (correlationId: CorrelationId) =\n        task {\n            let stopwatch = Stopwatch.StartNew()\n\n            try\n                let! blobClient = getAzureBlobClientForFileVersion repositoryDto fileVersion correlationId\n                let! existsResponse = blobClient.ExistsAsync()\n\n                if not existsResponse.Value then\n                    stopwatch.Stop()\n                    return MissingInStorage(fileVersion, stopwatch.Elapsed.TotalMilliseconds)\n                else\n                    // Download the stream from blob storage\n                    use! blobStream = blobClient.OpenReadAsync(position = 0, bufferSize = (64 * 1024))\n\n                    // Compute SHA-256 hash using the server-specific validation function.\n                    // Text files are stored as GZip streams and need decompression.\n                    let! computedHash =\n                        if fileVersion.IsBinary then\n                            computeSha256ForFile blobStream fileVersion.RelativePath\n                        else\n                            task {\n                                use gzStream = new GZipStream(stream = blobStream, mode = CompressionMode.Decompress, leaveOpen = false)\n                                return! computeSha256ForFile gzStream fileVersion.RelativePath\n                            }\n\n                    stopwatch.Stop()\n\n                    if computedHash = fileVersion.Sha256Hash then\n                        return Valid(fileVersion, computedHash, stopwatch.Elapsed.TotalMilliseconds)\n                    else\n                        return HashMismatch(fileVersion, fileVersion.Sha256Hash, computedHash, stopwatch.Elapsed.TotalMilliseconds)\n            with\n            | ex ->\n                stopwatch.Stop()\n                return ValidationError(fileVersion, ex.Message, stopwatch.Elapsed.TotalMilliseconds)\n        }\n\n    /// Determines which files need validation by comparing with a previously validated DirectoryVersion.\n    /// Returns the list of files that need to be validated.\n    let getFilesToValidate (newFiles: List<FileVersion>) (previouslyValidatedFiles: List<FileVersion>) : FileVersion array =\n        if previouslyValidatedFiles.Count > 0 then\n            // Create a set of (RelativePath, Sha256Hash) pairs from the old files\n            let previousFilesLookup = Dictionary<RelativePath, Sha256Hash>()\n\n            previouslyValidatedFiles\n            |> Seq.iter (fun previousFile -> previousFilesLookup.Add(previousFile.RelativePath, previousFile.Sha256Hash))\n\n            // Return files that are not in the old set (new or changed)\n            newFiles\n                .Where(fun f -> not (previousFilesLookup.Contains(KeyValuePair(f.RelativePath, f.Sha256Hash))))\n                .ToArray()\n        else\n            newFiles.ToArray()\n\n    type DirectoryVersionActor\n        (\n            [<PersistentState(StateName.DirectoryVersion, Constants.GraceActorStorage)>] state: IPersistentState<List<DirectoryVersionEvent>>\n        ) =\n        inherit Grain()\n\n        static let actorName = ActorName.DirectoryVersion\n\n        let log = loggerFactory.CreateLogger(\"DirectoryVersion.Actor\")\n\n        let mutable directoryVersionDto = DirectoryVersionDto.Default\n        let mutable currentCommand = String.Empty\n\n        /// Gets the name of the blob file that holds the cached recursive directory version list.\n        let getRecursiveDirectoryVersionsCacheFileName (directoryVersionId: DirectoryVersionId) = $\"{directoryVersionId}.msgpack\"\n\n        /// Gets the name of the blob file that holds the .zip file for the directory version.\n        let getZipFileBlobName (directoryVersionId: DirectoryVersionId) = $\"{GraceZipFilesFolderName}/{directoryVersionId}.zip\"\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            // Apply the events to build the current Dto.\n            directoryVersionDto <-\n                state.State\n                |> Seq.fold\n                    (fun directoryVersionDto directoryVersionEvent ->\n                        directoryVersionDto\n                        |> DirectoryVersionDto.UpdateDto directoryVersionEvent)\n                    DirectoryVersionDto.Default\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            Task.CompletedTask\n\n        interface IGraceReminderWithGuidKey with\n            /// Schedules a Grace reminder.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminder =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            directoryVersionDto.DirectoryVersion.OwnerId\n                            directoryVersionDto.DirectoryVersion.OrganizationId\n                            directoryVersionDto.DirectoryVersion.RepositoryId\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminder\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    this.correlationId <- reminder.CorrelationId\n\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.DeleteCachedState, ReminderState.DirectoryVersionDeleteCachedState reminderState ->\n                        this.correlationId <- reminderState.CorrelationId\n\n                        let directoryVersion = directoryVersionDto.DirectoryVersion\n\n                        let repositoryActorProxy =\n                            Repository.CreateActorProxy directoryVersion.OrganizationId directoryVersion.RepositoryId reminderState.CorrelationId\n\n                        let! repositoryDto = repositoryActorProxy.Get reminderState.CorrelationId\n\n                        // Delete cached state for this actor.\n                        let! directoryVersionBlobClient =\n                            getAzureBlobClient\n                                repositoryDto\n                                (getRecursiveDirectoryVersionsCacheFileName directoryVersion.DirectoryVersionId)\n                                reminderState.CorrelationId\n\n                        let! deleted = directoryVersionBlobClient.DeleteIfExistsAsync()\n\n                        if deleted.HasValue && deleted.Value then\n                            log.LogInformation(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Deleted cached state for directory version; RepositoryId: {RepositoryId}; DirectoryVersionId: {DirectoryVersionId}; deleteReason: {deleteReason}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                reminderState.CorrelationId,\n                                directoryVersion.RepositoryId,\n                                directoryVersion.DirectoryVersionId,\n                                reminderState.DeleteReason\n                            )\n                        else\n                            log.LogWarning(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Failed to delete cached state for directory version (it may have already been deleted); RepositoryId: {RepositoryId}; DirectoryVersionId: {DirectoryVersionId}; deleteReason: {deleteReason}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                reminderState.CorrelationId,\n                                directoryVersion.RepositoryId,\n                                directoryVersion.DirectoryVersionId,\n                                reminderState.DeleteReason\n                            )\n\n                        return Ok()\n                    | ReminderTypes.DeleteZipFile, ReminderState.DirectoryVersionDeleteZipFile reminderState ->\n                        this.correlationId <- reminderState.CorrelationId\n\n                        let directoryVersion = directoryVersionDto.DirectoryVersion\n\n                        let repositoryActorProxy =\n                            Repository.CreateActorProxy directoryVersion.OrganizationId directoryVersion.RepositoryId reminderState.CorrelationId\n\n                        let! repositoryDto = repositoryActorProxy.Get reminderState.CorrelationId\n\n                        // Delete zip file for this directory version.\n                        let blobName = getZipFileBlobName directoryVersion.DirectoryVersionId\n                        let! zipFileBlobClient = getAzureBlobClient repositoryDto blobName reminderState.CorrelationId\n\n                        let! deleted = zipFileBlobClient.DeleteIfExistsAsync()\n\n                        if deleted.HasValue && deleted.Value then\n                            log.LogInformation(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Deleted cache for directory version; RepositoryId: {RepositoryId}; DirectoryVersionId: {DirectoryVersionId}; deleteReason: {deleteReason}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                reminderState.CorrelationId,\n                                directoryVersionDto.DirectoryVersion.RepositoryId,\n                                directoryVersionDto.DirectoryVersion.DirectoryVersionId,\n                                reminderState.DeleteReason\n                            )\n                        else\n                            log.LogWarning(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Failed to delete cache for directory version (it may have already been deleted); RepositoryId: {RepositoryId}; DirectoryVersionId: {DirectoryVersionId}; deleteReason: {deleteReason}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                reminderState.CorrelationId,\n                                directoryVersionDto.DirectoryVersion.RepositoryId,\n                                directoryVersionDto.DirectoryVersion.DirectoryVersionId,\n                                reminderState.DeleteReason\n                            )\n\n                        return Ok()\n                    | ReminderTypes.PhysicalDeletion, ReminderState.DirectoryVersionPhysicalDeletion reminderState ->\n                        this.correlationId <- reminderState.CorrelationId\n\n                        // Delete saved state for this actor.\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Deleted state for directory version; RepositoryId: {RepositoryId}; DirectoryVersionId: {DirectoryVersionId}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            reminderState.CorrelationId,\n                            directoryVersionDto.DirectoryVersion.RepositoryId,\n                            directoryVersionDto.DirectoryVersion.DirectoryVersionId,\n                            reminderState.DeleteReason\n                        )\n\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                (GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId)\n                                    .enhance (\"IsRetryable\", \"false\")\n                            )\n                }\n\n        member private this.ApplyEvent directoryVersionEvent =\n            task {\n                try\n                    // Add the event to the list of events, and save it to actor state.\n                    state.State.Add(directoryVersionEvent)\n                    do! state.WriteStateAsync()\n\n                    // Update the Dto with the event.\n                    directoryVersionDto <-\n                        directoryVersionDto\n                        |> DirectoryVersionDto.UpdateDto directoryVersionEvent\n\n                    // Publish the event to the rest of the world.\n                    let graceEvent = GraceEvent.DirectoryVersionEvent directoryVersionEvent\n                    do! publishGraceEvent graceEvent directoryVersionEvent.Metadata\n\n                    let returnValue = GraceReturnValue.Create \"Directory version command succeeded.\" directoryVersionEvent.Metadata.CorrelationId\n\n                    returnValue\n                        .enhance(nameof RepositoryId, directoryVersionDto.DirectoryVersion.RepositoryId)\n                        .enhance(nameof DirectoryVersionId, directoryVersionDto.DirectoryVersion.DirectoryVersionId)\n                        .enhance(nameof Sha256Hash, directoryVersionDto.DirectoryVersion.Sha256Hash)\n                        .enhance (nameof DirectoryVersionEventType, getDiscriminatedUnionFullName directoryVersionEvent.Event)\n                    |> ignore\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    let graceError =\n                        GraceError.CreateWithException\n                            ex\n                            (getErrorMessage DirectoryVersionError.FailedWhileApplyingEvent)\n                            directoryVersionEvent.Metadata.CorrelationId\n\n                    graceError\n                        .enhance(nameof RepositoryId, directoryVersionDto.DirectoryVersion.RepositoryId)\n                        .enhance(nameof DirectoryVersionId, directoryVersionDto.DirectoryVersion.DirectoryVersionId)\n                        .enhance(nameof Sha256Hash, directoryVersionDto.DirectoryVersion.Sha256Hash)\n                        .enhance (nameof DirectoryVersionEventType, getDiscriminatedUnionFullName directoryVersionEvent.Event)\n                    |> ignore\n\n                    return Error graceError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId =\n                directoryVersionDto.DirectoryVersion.RepositoryId\n                |> returnTask\n\n        interface IDirectoryVersionActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                (directoryVersionDto.DirectoryVersion.DirectoryVersionId\n                 <> DirectoryVersion.Default.DirectoryVersionId)\n                |> returnTask\n\n            member this.Delete correlationId =\n                this.correlationId <- correlationId\n\n                GraceResult.Error(GraceError.Create \"Not implemented\" correlationId)\n                |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                directoryVersionDto |> returnTask\n\n            member this.GetCreatedAt correlationId =\n                this.correlationId <- correlationId\n\n                directoryVersionDto.DirectoryVersion.CreatedAt\n                |> returnTask\n\n            member this.GetDirectories correlationId =\n                this.correlationId <- correlationId\n\n                directoryVersionDto.DirectoryVersion.Directories\n                |> returnTask\n\n            member this.GetFiles correlationId =\n                this.correlationId <- correlationId\n\n                directoryVersionDto.DirectoryVersion.Files\n                |> returnTask\n\n            member this.GetSha256Hash correlationId =\n                this.correlationId <- correlationId\n\n                directoryVersionDto.DirectoryVersion.Sha256Hash\n                |> returnTask\n\n            member this.GetSize correlationId =\n                this.correlationId <- correlationId\n\n                directoryVersionDto.DirectoryVersion.Size\n                |> returnTask\n\n            member this.GetRecursiveDirectoryVersions (forceRegenerate: bool) correlationId =\n                this.correlationId <- correlationId\n\n                task {\n                    try\n                        let directoryVersion = directoryVersionDto.DirectoryVersion\n                        let repositoryActorProxy = Repository.CreateActorProxy directoryVersion.OrganizationId directoryVersion.RepositoryId correlationId\n                        let! repositoryDto = repositoryActorProxy.Get correlationId\n\n                        // Get the blob client for the cached recursive directory versions file.\n                        let! directoryVersionBlobClient =\n                            getAzureBlobClient\n                                repositoryDto\n                                (getRecursiveDirectoryVersionsCacheFileName directoryVersionDto.DirectoryVersion.DirectoryVersionId)\n                                correlationId\n\n                        // Check if the subdirectory versions have already been generated and cached.\n                        let cachedSubdirectoryVersions =\n                            task {\n                                if not forceRegenerate\n                                   && directoryVersionBlobClient.Exists() then\n                                    use! blobStream = directoryVersionBlobClient.OpenReadAsync()\n\n                                    let! directoryVersions =\n                                        MessagePackSerializer.DeserializeAsync<DirectoryVersionDto array>(blobStream, messagePackSerializerOptions)\n\n                                    return Some directoryVersions\n                                else\n                                    return None\n                            }\n\n                        // If they have already been generated, return them.\n                        match! cachedSubdirectoryVersions with\n                        | Some subdirectoryVersionDtos ->\n                            log.LogTrace(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}): Retrieved SubdirectoryVersions from cache.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                correlationId,\n                                this.IdentityString\n                            )\n\n                            return subdirectoryVersionDtos\n                        // If they haven't, generate them by calling each subdirectory in parallel.\n                        | None ->\n                            log.LogTrace(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}): subdirectoryVersions will be generated. forceRegenerate: {forceRegenerate}\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                correlationId,\n                                directoryVersionDto.DirectoryVersion.DirectoryVersionId,\n                                forceRegenerate\n                            )\n\n                            let subdirectoryVersionDtos = ConcurrentDictionary<RelativePath, DirectoryVersionDto>()\n\n                            log.LogTrace(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}): Adding current directory version. RelativePath: {relativePath}\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                correlationId,\n                                this.GetPrimaryKey(),\n                                directoryVersionDto.DirectoryVersion.RelativePath\n                            )\n\n                            // First, add the current directory version to the dictionary.\n                            subdirectoryVersionDtos.TryAdd(directoryVersionDto.DirectoryVersion.RelativePath, directoryVersionDto)\n                            |> ignore\n\n                            // Then, get the subdirectory versions in parallel and add them to the dictionary.\n                            do!\n                                Parallel.ForEachAsync(\n                                    directoryVersionDto.DirectoryVersion.Directories,\n                                    Constants.ParallelOptions,\n                                    (fun subdirectoryVersionId ct ->\n                                        ValueTask(\n                                            task {\n                                                try\n                                                    let subdirectoryActor =\n                                                        DirectoryVersion.CreateActorProxy\n                                                            subdirectoryVersionId\n                                                            directoryVersionDto.DirectoryVersion.RepositoryId\n                                                            correlationId\n\n                                                    // Get the contents of the subdirectory itself.\n                                                    let! subdirectoryVersion = subdirectoryActor.Get correlationId\n\n                                                    log.LogTrace(\n                                                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}): subdirectoryVersionId: {subdirectoryVersionId}. RelativePath: {relativePath}\\n{directoryVersion}\",\n                                                        getCurrentInstantExtended (),\n                                                        getMachineName,\n                                                        correlationId,\n                                                        directoryVersionDto.DirectoryVersion.DirectoryVersionId,\n                                                        subdirectoryVersionId,\n                                                        subdirectoryVersion.DirectoryVersion.RelativePath,\n                                                        serialize subdirectoryVersion\n                                                    )\n\n                                                    // Get the full recursive contents of the subdirectory.\n                                                    let! subdirectoryContents = subdirectoryActor.GetRecursiveDirectoryVersions forceRegenerate correlationId\n\n                                                    log.LogTrace(\n                                                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}): subdirectoryVersionId: {subdirectoryVersionId}; Retrieved {count} subdirectory versions for RelativePath: {relativePath}\\n{subdirectoryContents}\",\n                                                        getCurrentInstantExtended (),\n                                                        getMachineName,\n                                                        correlationId,\n                                                        directoryVersionDto.DirectoryVersion.DirectoryVersionId,\n                                                        subdirectoryVersionId,\n                                                        subdirectoryContents.Length,\n                                                        subdirectoryVersion.DirectoryVersion.RelativePath,\n                                                        serialize subdirectoryContents\n                                                    )\n\n                                                    for directoryVersionDto in subdirectoryContents do\n                                                        let directoryVersion = directoryVersionDto.DirectoryVersion\n\n                                                        log.LogTrace(\n                                                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}): subdirectoryVersionId: {subdirectoryVersionId}; Adding subdirectories. RelativePath: {relativePath}\",\n                                                            getCurrentInstantExtended (),\n                                                            getMachineName,\n                                                            correlationId,\n                                                            directoryVersion.DirectoryVersionId,\n                                                            subdirectoryVersionId,\n                                                            directoryVersion.RelativePath\n                                                        )\n\n                                                        subdirectoryVersionDtos.AddOrUpdate(\n                                                            directoryVersion.RelativePath,\n                                                            directoryVersionDto,\n                                                            (fun _ _ -> directoryVersionDto)\n                                                        )\n                                                        |> ignore\n                                                with\n                                                | ex ->\n                                                    log.LogError(\n                                                        \"{CurrentInstant}: Error in {methodName}; DirectoryId: {directoryId}; Exception: {exception}\",\n                                                        getCurrentInstantExtended (),\n                                                        \"GetRecursiveDirectoryVersions\",\n                                                        subdirectoryVersionId,\n                                                        ExceptionResponse.Create ex\n                                                    )\n                                            }\n                                        ))\n                                )\n\n                            // Sort the subdirectory versions by their relative path.\n                            let subdirectoryVersionsList =\n                                subdirectoryVersionDtos\n                                    .Values\n                                    .OrderBy(fun directoryVersionDto -> directoryVersionDto.DirectoryVersion.RelativePath)\n                                    .ToArray()\n\n                            // Save the recursive results to Azure Blob Storage.\n                            let repositoryActorProxy =\n                                Repository.CreateActorProxy\n                                    directoryVersionDto.DirectoryVersion.OrganizationId\n                                    directoryVersionDto.DirectoryVersion.RepositoryId\n                                    correlationId\n\n                            let! repositoryDto = repositoryActorProxy.Get correlationId\n\n                            let tags = Dictionary<string, string>()\n                            tags.Add(nameof OwnerId, $\"{repositoryDto.OwnerId}\")\n                            tags.Add(nameof OrganizationId, $\"{repositoryDto.OrganizationId}\")\n                            tags.Add(nameof RepositoryId, $\"{repositoryDto.RepositoryId}\")\n                            tags.Add(nameof DirectoryVersionId, $\"{directoryVersionDto.DirectoryVersion.DirectoryVersionId}\")\n                            tags.Add(nameof RelativePath, $\"{directoryVersionDto.DirectoryVersion.RelativePath}\")\n                            tags.Add(nameof Sha256Hash, $\"{directoryVersionDto.DirectoryVersion.Sha256Hash}\")\n                            tags.Add(\"RecursiveSize\", $\"{directoryVersionDto.RecursiveSize}\")\n\n                            // Write the JSON using MessagePack serialization for efficiency.\n                            let blockBlobOpenWriteOptions =\n                                BlockBlobOpenWriteOptions(Tags = tags, HttpHeaders = BlobHttpHeaders(ContentType = \"application/msgpack\"))\n\n                            let conditionsSummary =\n                                let conditionsProperty = typeof<BlockBlobOpenWriteOptions>.GetProperty(\"Conditions\")\n\n                                if isNull conditionsProperty then\n                                    \"not supported\"\n                                else\n                                    let conditionsValue = conditionsProperty.GetValue(blockBlobOpenWriteOptions)\n\n                                    if isNull conditionsValue then\n                                        \"null\"\n                                    else\n                                        let conditionProperties = conditionsValue.GetType().GetProperties()\n\n                                        conditionProperties\n                                        |> Seq.map (fun propertyInfo ->\n                                            let value = propertyInfo.GetValue(conditionsValue)\n                                            $\"{propertyInfo.Name}={value}\")\n                                        |> String.concat \"; \"\n\n                            log.LogDebug(\n                                \"In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}); Blob write conditions: {conditionsSummary}.\",\n                                this.GetPrimaryKey(),\n                                conditionsSummary\n                            )\n\n                            use! blobStream = directoryVersionBlobClient.OpenWriteAsync(overwrite = true, options = blockBlobOpenWriteOptions)\n                            do! MessagePackSerializer.SerializeAsync(blobStream, subdirectoryVersionsList, messagePackSerializerOptions)\n                            do! blobStream.DisposeAsync()\n\n                            log.LogDebug(\n                                \"In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}); Saving cached list of directory versions. RelativePath: {relativePath}.\",\n                                this.GetPrimaryKey(),\n                                directoryVersionDto.DirectoryVersion.RelativePath\n                            )\n\n                            // Create a reminder to delete the cached state after the configured number of cache days.\n                            let deletionReminderState: PhysicalDeletionReminderState =\n                                { DeleteReason = getDiscriminatedUnionCaseName ReminderTypes.DeleteCachedState; CorrelationId = correlationId }\n\n                            do!\n                                (this :> IGraceReminderWithGuidKey)\n                                    .ScheduleReminderAsync\n                                    ReminderTypes.DeleteCachedState\n                                    (Duration.FromDays(float repositoryDto.DirectoryVersionCacheDays))\n                                    (ReminderState.DirectoryVersionDeleteCachedState deletionReminderState)\n                                    correlationId\n\n                            log.LogDebug(\n                                \"In DirectoryVersionActor.GetRecursiveDirectoryVersions({id}); Delete cached state reminder was set.\",\n                                this.GetPrimaryKey()\n                            )\n\n                            return subdirectoryVersionsList\n                    with\n                    | ex ->\n                        log.LogError(\n                            \"{CurrentInstant}: Error in {methodName}. Exception: {exception}\",\n                            getCurrentInstantExtended (),\n                            \"GetRecursiveDirectoryVersions\",\n                            ExceptionResponse.Create ex\n                        )\n\n                        return Array.Empty<DirectoryVersionDto>()\n                }\n\n            member this.Handle command metadata =\n                let isValid command (metadata: EventMetadata) =\n                    task {\n                        match command with\n                        | DirectoryVersionCommand.Create (directoryVersion, repositoryDto) ->\n                            if\n                                state.State.Any (fun e ->\n                                    match e.Event with\n                                    | DirectoryVersionEventType.Created _ -> true\n                                    | _ -> false) then\n                                return\n                                    Error(\n                                        GraceError.Create\n                                            (DirectoryVersionError.getErrorMessage DirectoryVersionError.DirectoryAlreadyExists)\n                                            metadata.CorrelationId\n                                    )\n                            else\n                                return Ok command\n\n                        | _ ->\n                            if directoryVersionDto.DirectoryVersion.CreatedAt = DirectoryVersion.Default.CreatedAt then\n                                return Error(GraceError.Create (DirectoryVersionError.getErrorMessage DirectoryDoesNotExist) metadata.CorrelationId)\n                            else\n                                return Ok command\n                    }\n\n                let processCommand (command: DirectoryVersionCommand) (metadata: EventMetadata) =\n                    task {\n                        try\n                            let! event =\n                                task {\n                                    match command with\n                                    | Create (directoryVersion, repositoryDto) ->\n                                        // Determine which files need validation using incremental validation logic.\n                                        let! mostRecentDirectoryVersion =\n                                            getMostRecentDirectoryVersionByRelativePath\n                                                repositoryDto.RepositoryId\n                                                directoryVersion.RelativePath\n                                                metadata.CorrelationId\n\n                                        let filesToValidate =\n                                            match mostRecentDirectoryVersion with\n                                            | Some previousDirectoryVersion -> getFilesToValidate directoryVersion.Files previousDirectoryVersion.Files\n                                            | None -> getFilesToValidate directoryVersion.Files (List<FileVersion>())\n\n                                        log.LogDebug(\n                                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Starting SHA-256 validation for DirectoryVersion; DirectoryVersionId: {DirectoryVersionId}; RelativePath: {RelativePath}; FileCount: {FileCount}; FilesToValidate: {FilesToValidate}.\",\n                                            getCurrentInstantExtended (),\n                                            getMachineName,\n                                            metadata.CorrelationId,\n                                            directoryVersion.DirectoryVersionId,\n                                            directoryVersion.RelativePath,\n                                            directoryVersion.Files.Count,\n                                            filesToValidate.Length\n                                        )\n\n                                        let validationResults = ConcurrentQueue<FileValidationResult>()\n\n                                        // Validate files in parallel\n                                        do!\n                                            Parallel.ForEachAsync(\n                                                filesToValidate,\n                                                Constants.ParallelOptions,\n                                                (fun fileVersion ct ->\n                                                    ValueTask(\n                                                        task {\n                                                            let! result = validateFileSha256 repositoryDto fileVersion metadata.CorrelationId\n                                                            validationResults.Enqueue result\n                                                        }\n                                                    ))\n                                            )\n\n                                        let validationResults = validationResults.ToArray()\n\n                                        // Collect failures\n                                        let failures =\n                                            validationResults\n                                            |> Array.filter (fun result ->\n                                                match result with\n                                                | Valid _ -> false\n                                                | _ -> true)\n                                            |> Array.toList\n\n                                        let validCount =\n                                            validationResults\n                                            |> Array.filter (fun result ->\n                                                match result with\n                                                | Valid _ -> true\n                                                | _ -> false)\n                                            |> Array.length\n\n                                        let totalElapsedMs =\n                                            validationResults\n                                            |> Array.sumBy (fun result ->\n                                                match result with\n                                                | Valid (_, _, ms) -> ms\n                                                | HashMismatch (_, _, _, ms) -> ms\n                                                | MissingInStorage (_, ms) -> ms\n                                                | ValidationError (_, _, ms) -> ms)\n\n                                        // Log validation results\n                                        for result in validationResults do\n                                            match result with\n                                            | Valid (fv, computedHash, elapsedMs) ->\n                                                log.LogDebug(\n                                                    \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; SHA-256 validation passed; File: {RelativePath}; Hash: {Hash}; ElapsedMs: {ElapsedMs}.\",\n                                                    getCurrentInstantExtended (),\n                                                    getMachineName,\n                                                    metadata.CorrelationId,\n                                                    fv.RelativePath,\n                                                    computedHash,\n                                                    elapsedMs\n                                                )\n                                            | HashMismatch (fv, expectedHash, computedHash, elapsedMs) ->\n                                                log.LogWarning(\n                                                    \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; SHA-256 hash mismatch; File: {RelativePath}; ExpectedHash: {ExpectedHash}; ComputedHash: {ComputedHash}; ElapsedMs: {ElapsedMs}.\",\n                                                    getCurrentInstantExtended (),\n                                                    getMachineName,\n                                                    metadata.CorrelationId,\n                                                    fv.RelativePath,\n                                                    expectedHash,\n                                                    computedHash,\n                                                    elapsedMs\n                                                )\n                                            | MissingInStorage (fv, elapsedMs) ->\n                                                log.LogWarning(\n                                                    \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; File not found in object storage; File: {RelativePath}; ExpectedHash: {ExpectedHash}; ElapsedMs: {ElapsedMs}.\",\n                                                    getCurrentInstantExtended (),\n                                                    getMachineName,\n                                                    metadata.CorrelationId,\n                                                    fv.RelativePath,\n                                                    fv.Sha256Hash,\n                                                    elapsedMs\n                                                )\n                                            | ValidationError (fv, errorMessage, elapsedMs) ->\n                                                log.LogError(\n                                                    \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; SHA-256 validation error; File: {RelativePath}; Error: {ErrorMessage}; ElapsedMs: {ElapsedMs}.\",\n                                                    getCurrentInstantExtended (),\n                                                    getMachineName,\n                                                    metadata.CorrelationId,\n                                                    fv.RelativePath,\n                                                    errorMessage,\n                                                    elapsedMs\n                                                )\n\n                                        // Check if any validation failed\n                                        if failures.Length > 0 then\n                                            // Build error message with details about failures\n                                            let errorDetails =\n                                                failures\n                                                |> List.map (fun failure ->\n                                                    match failure with\n                                                    | HashMismatch (fv, expected, computed, _) ->\n                                                        $\"File '{fv.RelativePath}': hash mismatch (expected: {expected}, computed: {computed})\"\n                                                    | MissingInStorage (fv, _) -> $\"File '{fv.RelativePath}': not found in object storage\"\n                                                    | ValidationError (fv, msg, _) -> $\"File '{fv.RelativePath}': validation error ({msg})\"\n                                                    | _ -> \"Unknown error\")\n                                                |> String.concat \"; \"\n\n                                            log.LogWarning(\n                                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; SHA-256 validation failed for DirectoryVersion; DirectoryVersionId: {DirectoryVersionId}; RelativePath: {RelativePath}; FailedCount: {FailedCount}; ValidCount: {ValidCount}; TotalElapsedMs: {TotalElapsedMs}.\",\n                                                getCurrentInstantExtended (),\n                                                getMachineName,\n                                                metadata.CorrelationId,\n                                                directoryVersion.DirectoryVersionId,\n                                                directoryVersion.RelativePath,\n                                                failures.Length,\n                                                validCount,\n                                                totalElapsedMs\n                                            )\n\n                                            // Determine the appropriate error type\n                                            let hasHashMismatch =\n                                                failures\n                                                |> List.exists (fun f ->\n                                                    match f with\n                                                    | HashMismatch _ -> true\n                                                    | _ -> false)\n\n                                            let hasMissing =\n                                                failures\n                                                |> List.exists (fun f ->\n                                                    match f with\n                                                    | MissingInStorage _ -> true\n                                                    | _ -> false)\n\n                                            let errorMessage =\n                                                if hasMissing then\n                                                    DirectoryVersionError.getErrorMessage DirectoryVersionError.FileNotFoundInObjectStorage\n                                                    + \" \"\n                                                    + errorDetails\n                                                elif hasHashMismatch then\n                                                    DirectoryVersionError.getErrorMessage DirectoryVersionError.FileSha256HashDoesNotMatch\n                                                    + \" \"\n                                                    + errorDetails\n                                                else\n                                                    $\"File integrity check failed: {errorDetails}\"\n\n                                            return Error(GraceError.Create errorMessage metadata.CorrelationId)\n                                        else\n                                            log.LogInformation(\n                                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; SHA-256 validation succeeded for DirectoryVersion; DirectoryVersionId: {DirectoryVersionId}; RelativePath: {RelativePath}; ValidatedCount: {ValidatedCount}; TotalElapsedMs: {TotalElapsedMs}.\",\n                                                getCurrentInstantExtended (),\n                                                getMachineName,\n                                                metadata.CorrelationId,\n                                                directoryVersion.DirectoryVersionId,\n                                                directoryVersion.RelativePath,\n                                                validCount,\n                                                totalElapsedMs\n                                            )\n\n                                            let newDirectoryVersion = { directoryVersion with HashesValidated = true }\n                                            return Ok(Created newDirectoryVersion)\n                                    | SetRecursiveSize recursiveSize -> return Ok(RecursiveSizeSet recursiveSize)\n                                    | DeleteLogical deleteReason ->\n                                        let repositoryActorProxy =\n                                            Repository.CreateActorProxy\n                                                directoryVersionDto.DirectoryVersion.OrganizationId\n                                                directoryVersionDto.DirectoryVersion.RepositoryId\n                                                metadata.CorrelationId\n\n                                        let! repositoryDto = repositoryActorProxy.Get metadata.CorrelationId\n\n                                        let physicalDeletionReminderState =\n                                            { DeleteReason = getDiscriminatedUnionCaseName deleteReason; CorrelationId = metadata.CorrelationId }\n\n                                        do!\n                                            (this :> IGraceReminderWithGuidKey)\n                                                .ScheduleReminderAsync\n                                                ReminderTypes.PhysicalDeletion\n                                                (Duration.FromDays(float repositoryDto.LogicalDeleteDays))\n                                                (ReminderState.DirectoryVersionPhysicalDeletion physicalDeletionReminderState)\n                                                metadata.CorrelationId\n\n                                        return Ok(LogicalDeleted deleteReason)\n                                    | DeletePhysical ->\n                                        do! state.ClearStateAsync()\n                                        this.DeactivateOnIdle()\n                                        return Ok(PhysicalDeleted)\n                                    | Undelete -> return Ok(Undeleted)\n                                }\n\n                            match event with\n                            | Ok event -> return! this.ApplyEvent { Event = event; Metadata = metadata }\n                            | Error error -> return Error error\n                        with\n                        | ex ->\n                            let metadataObj = Dictionary<string, obj>(metadata.Properties.Select(fun kvp -> KeyValuePair<string, obj>(kvp.Key, kvp.Value)))\n                            return Error(GraceError.CreateWithMetadata ex String.Empty metadata.CorrelationId metadataObj)\n                    }\n\n                task {\n                    try\n                        this.correlationId <- metadata.CorrelationId\n                        currentCommand <- getDiscriminatedUnionCaseName command\n\n                        match! isValid command metadata with\n                        | Ok command -> return! processCommand command metadata\n                        | Error error -> return Error error\n                    with\n                    | ex ->\n                        logToConsole $\"Exception in DirectoryVersionActor.Handle(): {ExceptionResponse.Create ex}\"\n                        return Error(GraceError.CreateWithException ex \"Exception in DirectoryVersionActor.Handle()\" metadata.CorrelationId)\n                }\n\n            member this.GetRecursiveSize correlationId =\n                this.correlationId <- correlationId\n\n                task {\n                    if directoryVersionDto.RecursiveSize = Constants.InitialDirectorySize then\n                        let! directoryVersions =\n                            (this :> IDirectoryVersionActor)\n                                .GetRecursiveDirectoryVersions\n                                false\n                                correlationId\n\n                        let recursiveSize =\n                            directoryVersions\n                            |> Seq.sumBy (fun directoryVersionDto -> directoryVersionDto.DirectoryVersion.Size)\n\n                        match! (this :> IDirectoryVersionActor).Handle (SetRecursiveSize recursiveSize) (EventMetadata.New correlationId \"GraceSystem\") with\n                        | Ok returnValue -> return recursiveSize\n                        | Error error -> return Constants.InitialDirectorySize\n                    else\n                        return directoryVersionDto.RecursiveSize\n                }\n\n            member this.GetZipFileUri(correlationId: CorrelationId) : Task<UriWithSharedAccessSignature> =\n                this.correlationId <- correlationId\n                let directoryVersion = directoryVersionDto.DirectoryVersion\n\n                /// Creates a .zip file containing the file contents of the directory version.\n                let createDirectoryVersionZipFile\n                    (repositoryDto: RepositoryDto)\n                    (zipFileBlobName: string)\n                    (directoryVersionId: DirectoryVersionId)\n                    (subdirectoryVersionIds: List<DirectoryVersionId>)\n                    (fileVersions: IEnumerable<FileVersion>)\n                    =\n\n                    task {\n                        let zipFileName = $\"{directoryVersionId}.zip\"\n                        let tempZipPath = Path.Combine(Path.GetTempPath(), zipFileName)\n\n                        try\n                            // Step 1: Create the ZIP archive.\n                            use zipToCreate = new FileStream(tempZipPath, FileMode.Create, FileAccess.Write, FileShare.None, (64 * 1024))\n                            use archive = new ZipArchive(zipToCreate, ZipArchiveMode.Create)\n\n                            let zipFileUris = new ConcurrentDictionary<DirectoryVersionId, UriWithSharedAccessSignature>()\n\n                            // Step 2: Ensure that .zip files exist for all subdirectories, in parallel.\n                            do!\n                                Parallel.ForEachAsync(\n                                    subdirectoryVersionIds,\n                                    Constants.ParallelOptions,\n                                    fun subdirectoryVersionId ct ->\n                                        ValueTask(\n                                            task {\n                                                // Call the subdirectory actor to get the .zip file URI, which will create the .zip file if it doesn't already exist.\n                                                let subdirectoryActorProxy =\n                                                    DirectoryVersion.CreateActorProxy\n                                                        subdirectoryVersionId\n                                                        directoryVersionDto.DirectoryVersion.RepositoryId\n                                                        correlationId\n\n                                                let! subdirectoryZipFileUri = subdirectoryActorProxy.GetZipFileUri correlationId\n                                                zipFileUris[subdirectoryVersionId] <- subdirectoryZipFileUri\n                                            }\n                                        )\n                                )\n\n                            // Step 3: Process the subdirectories of the current directory one at a time, because we need to add entries to the .zip file one at a time.\n                            for subdirectoryVersionId in subdirectoryVersionIds do\n                                // Get an Azure Blob Client for the .zip file.\n                                let subdirectoryZipFileName = getZipFileBlobName subdirectoryVersionId\n                                let! subdirectoryZipFileClient = getAzureBlobClient repositoryDto subdirectoryZipFileName correlationId\n\n                                // Copy the contents of the subdirectory's .zip file to the new .zip we're creating.\n                                use! subdirectoryZipFileStream = subdirectoryZipFileClient.OpenReadAsync()\n                                use subdirectoryZipArchive = new ZipArchive(subdirectoryZipFileStream, ZipArchiveMode.Read)\n\n                                for entry in subdirectoryZipArchive.Entries do\n                                    if not (String.IsNullOrEmpty(entry.Name)) then\n                                        // Using CompressionLevel.NoCompression because the files are already GZipped.\n                                        // We're just using .zip as an archive format for already-compressed files.\n                                        let newEntry = archive.CreateEntry(entry.FullName, CompressionLevel.NoCompression)\n                                        newEntry.Comment <- entry.Comment\n                                        use entryStream = entry.Open()\n                                        use newEntryStream = newEntry.Open()\n                                        do! entryStream.CopyToAsync(newEntryStream)\n\n                            // Step 4: Process the files in the current directory.\n                            for fileVersion in fileVersions do\n\n                                let! fileBlobClient = getAzureBlobClientForFileVersion repositoryDto fileVersion correlationId\n                                let! existsResult = fileBlobClient.ExistsAsync()\n\n                                if existsResult.Value = true then\n                                    use! fileStream = fileBlobClient.OpenReadAsync()\n                                    let zipEntry = archive.CreateEntry(fileVersion.RelativePath, CompressionLevel.NoCompression)\n                                    zipEntry.Comment <- fileVersion.GetObjectFileName\n                                    use zipEntryStream = zipEntry.Open()\n                                    do! fileStream.CopyToAsync(zipEntryStream)\n\n                            // Step 5: Upload the new ZIP to Azure Blob Storage\n                            archive.Dispose() // Dispose the archive before uploading to ensure it's properly flushed to the disk.\n                            let! zipFileBlobClient = getAzureBlobClient repositoryDto zipFileBlobName correlationId\n                            use tempZipFileStream = File.OpenRead(tempZipPath)\n                            let! response = zipFileBlobClient.UploadAsync(tempZipFileStream)\n                            ()\n                        finally\n                            // Step 5: Delete the local ZIP file\n                            if File.Exists(tempZipPath) then File.Delete(tempZipPath)\n                    }\n\n                task {\n                    logToConsole $\"In GetZipFileUri: DirectoryVersionId: {directoryVersion.DirectoryVersionId}; RelativePath: {directoryVersion.RelativePath}.\"\n                    let repositoryActorProxy = Repository.CreateActorProxy directoryVersion.OrganizationId directoryVersion.RepositoryId correlationId\n                    let! repositoryDto = repositoryActorProxy.Get correlationId\n\n                    let blobName = getZipFileBlobName directoryVersion.DirectoryVersionId\n                    let! zipFileBlobClient = getAzureBlobClient repositoryDto blobName correlationId\n\n                    let! zipFileExists = zipFileBlobClient.ExistsAsync()\n\n                    if zipFileExists.Value = true then\n                        // We already have this .zip file, so just return the URI with SAS.\n                        logToConsole\n                            $\"In GetZipFileUri: .zip file already exists for DirectoryVersionId: {directoryVersion.DirectoryVersionId}; RelativePath: {directoryVersion.RelativePath}.\"\n\n                        let! uriWithSas = getUriWithReadSharedAccessSignature repositoryDto blobName correlationId\n                        return uriWithSas\n                    else\n                        // We don't have the .zip file saved, so let's create it.\n                        logToConsole\n                            $\"In GetZipFileUri: Creating .zip file for DirectoryVersionId: {directoryVersion.DirectoryVersionId}; RelativePath: {directoryVersion.RelativePath}.\"\n\n                        do!\n                            createDirectoryVersionZipFile\n                                repositoryDto\n                                blobName\n                                directoryVersion.DirectoryVersionId\n                                directoryVersion.Directories\n                                directoryVersion.Files\n\n                        // Schedule a reminder to delete the .zip file after the cache days have passed.\n                        let deletionReminderState: PhysicalDeletionReminderState =\n                            { DeleteReason = getDiscriminatedUnionCaseName DeleteZipFile; CorrelationId = correlationId }\n\n                        do!\n                            (this :> IGraceReminderWithGuidKey)\n                                .ScheduleReminderAsync\n                                DeleteZipFile\n                                (Duration.FromDays(float repositoryDto.DirectoryVersionCacheDays))\n                                (ReminderState.DirectoryVersionDeleteZipFile deletionReminderState)\n                                correlationId\n\n                        let! uriWithSas = getUriWithReadSharedAccessSignature repositoryDto blobName correlationId\n                        return uriWithSas\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Extensions/MemoryCache.Extensions.Actor.fs",
    "content": "namespace Grace.Actors.Extensions\n\nopen Grace.Shared.Constants\nopen Grace.Types.Types\nopen Orleans.Runtime\nopen Microsoft.Extensions.Caching.Memory\nopen System\nopen System.Collections.Generic\nopen Grace.Shared\nopen Grace.Shared.Validation\n\nmodule MemoryCache =\n    [<Literal>]\n    let ownerIdPrefix = \"OwI\"\n\n    [<Literal>]\n    let organizationIdPrefix = \"OrI\"\n\n    [<Literal>]\n    let repositoryIdPrefix = \"ReI\"\n\n    [<Literal>]\n    let branchIdPrefix = \"BrI\"\n\n    [<Literal>]\n    let ownerNamePrefix = \"OwN\"\n\n    [<Literal>]\n    let organizationNamePrefix = \"OrN\"\n\n    [<Literal>]\n    let repositoryNamePrefix = \"ReN\"\n\n    [<Literal>]\n    let branchNamePrefix = \"BrN\"\n\n    [<Literal>]\n    let correlationIdPrefix = \"CoI\"\n\n    [<Literal>]\n    let orleansContextPrefix = \"Orl\"\n\n    type Microsoft.Extensions.Caching.Memory.IMemoryCache with\n\n        /// Get a value from MemoryCache, if it exists.\n        member this.GetFromCache<'T>(key: string) =\n            let mutable value = Unchecked.defaultof<'T>\n            if this.TryGetValue(key, &value) then Some value else None\n\n        /// Create a new entry in MemoryCache with a default expiration time.\n        member this.CreateWithDefaultExpirationTime (key: string) value =\n            use newCacheEntry = this.CreateEntry(key, Value = value, AbsoluteExpiration = DateTimeOffset.UtcNow.Add MemoryCache.DefaultExpirationTime)\n            //Utilities.logToConsole $\"In CreateWithDefaultExpirationTime: {key}: {value}\"\n            ()\n\n        /// Create a new entry in MemoryCache to link an ActorId with a CorrelationId.\n        member this.CreateCorrelationIdEntry (identityString: string) (correlationId: CorrelationId) =\n            use newCacheEntry =\n                this.CreateEntry($\"{correlationIdPrefix}:{identityString}\", Value = correlationId, AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds 5)\n\n            ()\n\n        /// Check if we have an entry in MemoryCache for an ActorId, and return the CorrelationId if we have it.\n        member this.GetCorrelationIdEntry(identityString: string) = this.GetFromCache<string> $\"{correlationIdPrefix}:{identityString}\"\n\n        /// Create a new entry in MemoryCache to confirm that an OwnerId exists.\n        member this.CreateOwnerIdEntry (ownerId: OwnerId) (value: string) = this.CreateWithDefaultExpirationTime $\"{ownerIdPrefix}:{ownerId}\" value\n\n        /// Check if we have an entry in MemoryCache for an OwnerId.\n        member this.GetOwnerIdEntry(ownerId: OwnerId) = this.GetFromCache<string> $\"{ownerIdPrefix}:{ownerId}\"\n\n        /// Remove an entry in MemoryCache for an OwnerId.\n        member this.RemoveOwnerIdEntry(ownerId: OwnerId) = this.Remove($\"{ownerIdPrefix}:{ownerId}\")\n\n\n        /// Create a new entry in MemoryCache to confirm that an OwnerId has been deleted.\n        member this.CreateDeletedOwnerIdEntry (ownerId: OwnerId) (value: string) =\n            this.CreateWithDefaultExpirationTime $\"{ownerIdPrefix}:{ownerId}:Deleted\" value\n\n        /// Check if we have an entry in MemoryCache for a deleted OwnerId.\n        member this.GetDeletedOwnerIdEntry(ownerId: OwnerId) = this.GetFromCache<string> $\"{ownerIdPrefix}:{ownerId}:Deleted\"\n\n        /// Remove an entry in MemoryCache for a deleted OwnerId.\n        member this.RemoveDeletedOwnerIdEntry(ownerId: OwnerId) = this.Remove($\"{ownerIdPrefix}:{ownerId}:Deleted\")\n\n\n        /// Create a new entry in MemoryCache to confirm that an OrganizationId exists.\n        member this.CreateOrganizationIdEntry (organizationId: OrganizationId) (value: string) =\n            this.CreateWithDefaultExpirationTime $\"{organizationIdPrefix}:{organizationId}\" value\n\n        /// Check if we have an entry in MemoryCache for an OrganizationId.\n        member this.GetOrganizationIdEntry(organizationId: OrganizationId) = this.GetFromCache<string> $\"{organizationIdPrefix}:{organizationId}\"\n\n        /// Remove an entry in MemoryCache for an OrganizationId.\n        member this.RemoveOrganizationIdEntry(organizationId: OrganizationId) = this.Remove($\"{organizationIdPrefix}:{organizationId}\")\n\n\n        /// Create a new entry in MemoryCache to confirm that an OrganizationId has been deleted.\n        member this.CreateDeletedOrganizationIdEntry (organizationId: OrganizationId) (value: string) =\n            this.CreateWithDefaultExpirationTime $\"{organizationIdPrefix}:{organizationId}:Deleted\" value\n\n        /// Check if we have an entry in MemoryCache for a deleted OrganizationId.\n        member this.GetDeletedOrganizationIdEntry(organizationId: OrganizationId) = this.GetFromCache<string> $\"{organizationIdPrefix}:{organizationId}:Deleted\"\n\n        /// Remove an entry in MemoryCache for a deleted OrganizationId.\n        member this.RemoveDeletedOrganizationIdEntry(organizationId: OrganizationId) = this.Remove($\"{organizationIdPrefix}:{organizationId}:Deleted\")\n\n\n        /// Create a new entry in MemoryCache to confirm that a RepositoryId exists.\n        member this.CreateRepositoryIdEntry (repositoryId: RepositoryId) (value: string) =\n            this.CreateWithDefaultExpirationTime $\"{repositoryIdPrefix}:{repositoryId}\" value\n\n        /// Check if we have an entry in MemoryCache for a RepositoryId.\n        member this.GetRepositoryIdEntry(repositoryId: RepositoryId) = this.GetFromCache<string> $\"{repositoryIdPrefix}:{repositoryId}\"\n\n        /// Remove an entry in MemoryCache for a RepositoryId.\n        member this.RemoveRepositoryIdEntry(repositoryId: RepositoryId) = this.Remove($\"{repositoryIdPrefix}:{repositoryId}\")\n\n\n        /// Create a new entry in MemoryCache to confirm that a RepositoryId has been deleted.\n        member this.CreateDeletedRepositoryIdEntry (repositoryId: RepositoryId) (value: string) =\n            this.CreateWithDefaultExpirationTime $\"{repositoryIdPrefix}:{repositoryId}:Deleted\" value\n\n        /// Check if we have an entry in MemoryCache for a deleted RepositoryId.\n        member this.GetDeletedRepositoryIdEntry(repositoryId: RepositoryId) = this.GetFromCache<string> $\"{repositoryIdPrefix}:{repositoryId}:Deleted\"\n\n        /// Remove an entry in MemoryCache for a deleted RepositoryId.\n        member this.RemoveDeletedRepositoryIdEntry(repositoryId: RepositoryId) = this.Remove($\"{repositoryIdPrefix}:{repositoryId}:Deleted\")\n\n\n        /// Create a new entry in MemoryCache to confirm that a BranchId exists.\n        member this.CreateBranchIdEntry (branchId: BranchId) (value: string) = this.CreateWithDefaultExpirationTime $\"{branchIdPrefix}:{branchId}\" value\n\n        /// Check if we have an entry in MemoryCache for a BranchId.\n        member this.GetBranchIdEntry(branchId: BranchId) = this.GetFromCache<string> $\"{branchIdPrefix}:{branchId}\"\n\n        /// Remove an entry in MemoryCache for a BranchId.\n        member this.RemoveBranchIdEntry(branchId: BranchId) = this.Remove($\"{branchIdPrefix}:{branchId}\")\n\n\n        /// Create a new entry in MemoryCache to confirm that a BranchId has been deleted.\n        member this.CreateDeletedBranchIdEntry (branchId: BranchId) (value: string) =\n            this.CreateWithDefaultExpirationTime $\"{branchIdPrefix}:{branchId}:Deleted\" value\n\n        /// Check if we have an entry in MemoryCache for a deleted BranchId.\n        member this.GetDeletedBranchIdEntry(branchId: BranchId) = this.GetFromCache<string> $\"{branchIdPrefix}:{branchId}:Deleted\"\n\n        /// Remove an entry in MemoryCache for a deleted BranchId.\n        member this.RemoveDeletedBranchIdEntry(branchId: BranchId) = this.Remove($\"{branchIdPrefix}:{branchId}:Deleted\")\n\n\n        /// Create a new entry in MemoryCache to link an OwnerName with an OwnerId.\n        member this.CreateOwnerNameEntry (ownerName: OwnerName) (ownerId: OwnerId) =\n            this.CreateWithDefaultExpirationTime $\"{ownerNamePrefix}:{ownerName}\" ownerId\n\n        /// Check if we have an entry in MemoryCache for an OwnerName, and return the OwnerId if we have it.\n        member this.GetOwnerNameEntry(ownerName: string) = this.GetFromCache<OwnerId> $\"{ownerNamePrefix}:{ownerName}\"\n\n        /// Remove an entry in MemoryCache for an OwnerName.\n        member this.RemoveOwnerNameEntry(ownerName: string) = this.Remove($\"{ownerNamePrefix}:{ownerName}\")\n\n\n        /// Create a new entry in MemoryCache to link an OrganizationName with an OrganizationId.\n        member this.CreateOrganizationNameEntry (organizationName: OrganizationName) (organizationId: OrganizationId) =\n            this.CreateWithDefaultExpirationTime $\"{organizationNamePrefix}:{organizationName}\" organizationId\n\n        /// Check if we have an entry in MemoryCache for an OrganizationName, and return the OrganizationId if we have it.\n        member this.GetOrganizationNameEntry(organizationName: string) = this.GetFromCache<OrganizationId> $\"{organizationNamePrefix}:{organizationName}\"\n\n        /// Remove an entry in MemoryCache for an OrganizationName.\n        member this.RemoveOrganizationNameEntry(organizationName: string) = this.Remove($\"{organizationNamePrefix}:{organizationName}\")\n\n\n        /// Create a new entry in MemoryCache to link a RepositoryName with a RepositoryId.\n        member this.CreateRepositoryNameEntry (repositoryName: RepositoryName) (repositoryId: RepositoryId) =\n            this.CreateWithDefaultExpirationTime $\"{repositoryNamePrefix}:{repositoryName}\" repositoryId\n\n        /// Check if we have an entry in MemoryCache for a RepositoryName, and return the RepositoryId if we have it.\n        member this.GetRepositoryNameEntry(repositoryName: string) = this.GetFromCache<RepositoryId> $\"{repositoryNamePrefix}:{repositoryName}\"\n\n        /// Remove an entry in MemoryCache for a RepositoryName.\n        member this.RemoveRepositoryNameEntry(repositoryName: string) = this.Remove($\"{repositoryNamePrefix}:{repositoryName}\")\n\n\n        /// Create a new entry in MemoryCache to link a BranchName with a BranchId.\n        member this.CreateBranchNameEntry(repositoryId: string, branchName: string, branchId: BranchId) =\n            this.CreateWithDefaultExpirationTime $\"{branchNamePrefix}:{repositoryId}-{branchName}\" branchId\n\n        /// Create a new entry in MemoryCache to link a BranchName with a BranchId.\n        member this.CreateBranchNameEntry(repositoryId: RepositoryId, branchName: string, branchId: BranchId) =\n            this.CreateWithDefaultExpirationTime $\"{branchNamePrefix}:{repositoryId}-{branchName}\" branchId\n\n        /// Check if we have an entry in MemoryCache for a BranchName, and return the BranchId if we have it.\n        member this.GetBranchNameEntry(repositoryId: string, branchName: string) = this.GetFromCache<BranchId> $\"{branchNamePrefix}:{repositoryId}-{branchName}\"\n\n        /// Check if we have an entry in MemoryCache for a BranchName, and return the BranchId if we have it.\n        member this.GetBranchNameEntry(repositoryId: RepositoryId, branchName: string) =\n            this.GetFromCache<BranchId> $\"{branchNamePrefix}:{repositoryId}-{branchName}\"\n\n        /// Remove an entry in MemoryCache for a BranchName.\n        member this.RemoveBranchNameEntry(repositoryId: string, branchName: string) = this.Remove($\"{branchNamePrefix}:{repositoryId}-{branchName}\")\n\n        /// Remove an entry in MemoryCache for a BranchName.\n        member this.RemoveBranchNameEntry(repositoryId: RepositoryId, branchName: string) = this.Remove($\"{branchNamePrefix}:{repositoryId}-{branchName}\")\n\n\n        /// Create a new entry in MemoryCache to store the current thread count information.\n        member this.CreateThreadCountEntry(threadInfo: string) =\n            use newCacheEntry = this.CreateEntry(\"ThreadCounts\", Value = threadInfo, AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds 6)\n            ()\n\n        /// Check if we have an entry in MemoryCache for the current ThreadCount.\n        member this.GetThreadCountEntry() = this.GetFromCache<string> \"ThreadCounts\"\n\n        /// Create a new entry in MemoryCache to store context information for an Orleans grain.\n        member this.CreateOrleansContextEntry(grainId: GrainId, orleansContext: Dictionary<string, obj>) =\n            use newCacheEntry =\n                this.CreateEntry($\"{orleansContextPrefix}:{grainId}\", Value = orleansContext, AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds 60)\n\n            ()\n\n        /// Check if we have an entry in MemoryCache for the Orleans context, and return the value if we have it.\n        member this.GetOrleansContextEntry(grainId: GrainId) =\n            this.GetFromCache<Dictionary<string, obj>> $\"{orleansContextPrefix}:{grainId}\"\n            |> Option.bind (fun orleansContext -> Some(orleansContext :> IReadOnlyDictionary<string, obj>))\n"
  },
  {
    "path": "src/Grace.Actors/FileAppearance.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule FileAppearance =\n\n    let actorName = ActorName.FileAppearance\n    //let log = loggerFactory.CreateLogger(\"FileAppearance.Actor\")\n\n    type FileAppearanceDto() =\n        member val public Appearances = SortedSet<Appearance>() with get, set\n\n    type FileAppearanceActor([<PersistentState(StateName.FileAppearance, Constants.GraceActorStorage)>] state: IPersistentState<SortedSet<Appearance>>) =\n        inherit Grain()\n\n        let log = loggerFactory.CreateLogger(\"FileAppearance.Actor\")\n\n        let mutable correlationId = String.Empty\n\n        let dtoStateName = StateName.FileAppearance\n        let mutable dto = FileAppearanceDto()\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            Task.CompletedTask\n\n        interface IFileAppearanceActor with\n            member this.Add appearance correlationId =\n                task {\n                    let wasAdded = dto.Appearances.Add(appearance)\n\n                    if wasAdded then do! state.WriteStateAsync()\n                }\n                :> Task\n\n            member this.Remove appearance correlationId =\n                task {\n                    let wasRemoved = dto.Appearances.Remove(appearance)\n\n                    if wasRemoved then\n                        if dto.Appearances |> Seq.isEmpty then\n                            // TODO: Delete the file from storage\n                            do! state.ClearStateAsync()\n                        else\n                            do! state.WriteStateAsync()\n\n                        ()\n                }\n                :> Task\n\n            member this.Contains appearance correlationId = Task.FromResult(dto.Appearances.Contains(appearance))\n\n            member this.Appearances correlationId = Task.FromResult(dto.Appearances)\n"
  },
  {
    "path": "src/Grace.Actors/GlobalLock.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule GlobalLock =\n\n    type LockState =\n        {\n            IsLocked: bool\n            LockedBy: string option\n            LockedAt: Instant option\n        }\n\n        static member Unlocked = { IsLocked = false; LockedBy = None; LockedAt = None }\n\n    let log = loggerFactory.CreateLogger(\"GlobalLock.Actor\")\n\n    type GlobalLockActor() =\n        inherit Grain()\n\n        static let actorName = ActorName.GlobalLock\n\n        let log = loggerFactory.CreateLogger(\"GlobalLock.Actor\")\n\n        let mutable actorStartTime = Instant.MinValue\n\n        let mutable lockState = LockState.Unlocked\n        let mutable instanceName = String.Empty\n\n        interface IGlobalLockActor with\n            member this.AcquireLock(lockedBy: string) =\n                if lockState.IsLocked then\n                    false |> returnTask\n                else\n                    lockState <- { IsLocked = true; LockedBy = Some lockedBy; LockedAt = Some(getCurrentInstant ()) }\n                    instanceName <- lockedBy\n                    true |> returnTask\n\n            member this.ReleaseLock(releasedBy: string) =\n                match lockState.LockedBy with\n                | Some lockedBy ->\n                    if lockState.IsLocked && lockedBy = releasedBy then\n                        lockState <- LockState.Unlocked\n                        instanceName <- lockedBy\n                        Ok() |> returnTask\n                    else\n                        Error \"Not locked by the calling instance.\"\n                        |> returnTask\n                | None ->\n                    Error \"Cannot release the lock. The lock has not been acquired.\"\n                    |> returnTask //blah\n\n            member this.IsLocked() = lockState.IsLocked |> returnTask\n"
  },
  {
    "path": "src/Grace.Actors/Grace.Actors.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>net10.0</TargetFramework>\n        <LangVersion>preview</LangVersion>\n        <PublishReadyToRun>false</PublishReadyToRun>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n        <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <WarningsAsErrors>FS0025</WarningsAsErrors>\n        <NoWarn>1057,3391</NoWarn>\n        <Platforms>AnyCPU;x64</Platforms>\n        <OtherFlags>--test:GraphBasedChecking</OtherFlags>\n        <OtherFlags>--test:ParallelOptimization</OtherFlags>\n        <OtherFlags>--test:ParallelIlxGen</OtherFlags>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <None Include=\"instructions.md\" />\n        <Compile Include=\"Extensions\\MemoryCache.Extensions.Actor.fs\" />\n        <Compile Include=\"Constants.Actor.fs\" />\n        <Compile Include=\"Types.Actor.fs\" />\n        <Compile Include=\"Interfaces.Actor.fs\" />\n        <Compile Include=\"Context.Actor.fs\" />\n        <Compile Include=\"PersonalAccessToken.Actor.fs\" />\n        <Compile Include=\"Timing.Actor.fs\" />\n        <Compile Include=\"ActorProxy.Extensions.Actor.fs\" />\n        <Compile Include=\"Services.Actor.fs\" />\n        <Compile Include=\"GrainRepository.Actor.fs\" />\n        <Compile Include=\"GlobalLock.Actor.fs\" />\n        <Compile Include=\"AccessControl.Actor.fs\" />\n        <Compile Include=\"Reminder.Actor.fs\" />\n        <Compile Include=\"User.Actor.fs\" />\n        <Compile Include=\"Owner.Actor.fs\" />\n        <Compile Include=\"OwnerName.Actor.fs\" />\n        <Compile Include=\"Organization.Actor.fs\" />\n        <Compile Include=\"OrganizationName.Actor.fs\" />\n        <Compile Include=\"NamedSection.Actor.fs\" />\n        <Compile Include=\"DirectoryVersion.Actor.fs\" />\n        <Compile Include=\"Reference.Actor.fs\" />\n        <Compile Include=\"Branch.Actor.fs\" />\n        <Compile Include=\"BranchName.Actor.fs\" />\n        \n        <Compile Include=\"PromotionSet.Actor.fs\" />\n        <Compile Include=\"WorkItemNumber.Actor.fs\" />\n        <Compile Include=\"WorkItemNumberCounter.Actor.fs\" />\n        <Compile Include=\"WorkItem.Actor.fs\" />\n        <Compile Include=\"Policy.Actor.fs\" />\n        <Compile Include=\"Review.Actor.fs\" />\n        <Compile Include=\"ValidationSet.Actor.fs\" />\n        <Compile Include=\"ValidationResult.Actor.fs\" />\n        <Compile Include=\"Artifact.Actor.fs\" />\n        <Compile Include=\"PromotionQueue.Actor.fs\" />\n        <Compile Include=\"Repository.Actor.fs\" />\n        <Compile Include=\"RepositoryName.Actor.fs\" />\n        <Compile Include=\"RepositoryPermission.Actor.fs\" />\n        <Compile Include=\"Diff.Actor.fs\" />\n        <Compile Include=\"FileAppearance.Actor.fs\" />\n        <Compile Include=\"DirectoryAppearance.Actor.fs\" />\n        <Compile Include=\"CodeGenAttribute.Actor.fs\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <None Include=\"Repository.Actor.fs %28ApplyEvent Method%29\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"Azure.Messaging.ServiceBus\" Version=\"7.20.1\" />\n        <PackageReference Include=\"Azure.Storage.Blobs\" Version=\"12.26.0\" />\n        <PackageReference Include=\"Azure.Storage.Blobs.Batch\" Version=\"12.23.0\" />\n        <PackageReference Include=\"Azure.Storage.Common\" Version=\"12.25.0\" />\n        <PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n        <PackageReference Include=\"FSharpPlus\" Version=\"1.8.0\" />\n        <PackageReference Include=\"Microsoft.AspNetCore.SignalR\" Version=\"1.2.0\" />\n        <PackageReference Include=\"Microsoft.Azure.Cosmos\" Version=\"3.56.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.0\" />\n        <PackageReference Include=\"Microsoft.Orleans.Core\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Runtime\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Serialization.FSharp\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Serialization.SystemTextJson\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Streaming.AzureStorage\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.4\" />\n        <PackageReference Include=\"Nito.AsyncEx\" Version=\"5.1.2\" />\n        <PackageReference Include=\"NodaTime\" Version=\"3.2.2\" />\n        <PackageReference Include=\"NodaTime.Serialization.SystemTextJson\" Version=\"1.3.0\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n        <ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <InternalsVisibleTo Include=\"Grace.Server.Tests\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Actors/GrainRepository.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule GrainRepository =\n\n    type GrainRepositoryActor() =\n        inherit Grain()\n\n        static let actorName = ActorName.RepositoryName\n\n        let log = loggerFactory.CreateLogger(\"GrainRepository.Actor\")\n\n        let mutable cachedRepositoryId: RepositoryId option = None\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        //override this.OnActivateAsync(ct) =\n        //    logActorActivation log this.IdentityString \"In-memory only\"\n        //    Task.CompletedTask\n\n        interface IGrainRepositoryActor with\n            member this.GetRepositoryId correlationId =\n                this.correlationId <- correlationId\n                cachedRepositoryId |> returnTask\n\n            member this.SetRepositoryId (repositoryId: RepositoryId) correlationId =\n                this.correlationId <- correlationId\n                cachedRepositoryId <- Some repositoryId\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/Interfaces.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Types.Authorization\nopen Grace.Types.Branch\nopen Grace.Types.Diff\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Reference\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.Organization\nopen Grace.Types.Owner\nopen Grace.Types.PersonalAccessToken\nopen Grace.Types.Policy\nopen Grace.Types.PromotionSet\nopen Grace.Types.Review\nopen Grace.Types.Queue\nopen Grace.Types.Validation\nopen Grace.Types.Artifact\nopen Grace.Types.WorkItem\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen Orleans\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\nopen System.Reflection.Metadata\nopen Orleans.Runtime\n\nmodule Interfaces =\n\n    type PersistAction =\n        | Save\n        | DoNotSave\n\n    type ExportError =\n        | EventListIsEmpty\n        | Exception of ExceptionResponse\n\n    type ImportError =\n        | EventListIsEmpty\n        | Exception of ExceptionResponse\n\n    type RevertError =\n        | EmptyEventList\n        | OutOfRange\n        | Exception of ExceptionResponse\n\n    /// Retrieves the RepositoryId for a grain.\n    /// This is used when computing the PartitionKey for grains in a repository during storage operations.\n    [<Interface>]\n    type IGrainRepositoryIdExtension =\n        inherit IGrainExtension\n\n        /// Retrieves the RepositoryId for a grain.\n        abstract member GetRepositoryId: correlationId: CorrelationId -> Task<RepositoryId>\n\n    /// Retrieves the RepositoryId for a grain.\n    /// This is used when computing the PartitionKey for grains in a repository during storage operations.\n    [<Interface>]\n    type IHasRepositoryId =\n        /// Gets the RepositoryId for this actor.\n        abstract member GetRepositoryId: correlationId: CorrelationId -> Task<RepositoryId>\n\n    /// This is an experimental interface to explore how to back up and rehydrate actor instances.\n    [<Interface>]\n    type IExportable<'T> =\n        abstract member Export: unit -> Task<Result<List<'T>, ExportError>>\n        abstract member Import: IReadOnlyList<'T> -> Task<Result<int, ImportError>>\n\n    /// This is an experimental interface to explore how to implement important management functions for actors that we'll need in production.\n    [<Interface>]\n    type IRevertable<'T> =\n        abstract member EventCount: unit -> Task<int>\n        abstract member RevertToInstant: Instant -> PersistAction -> Task<Result<'T, RevertError>>\n        abstract member RevertBack: int -> PersistAction -> Task<Result<'T, RevertError>>\n\n    /// Defines the operations that an actor must implement to handle Grace reminders.\n    [<Interface>]\n    type IGraceReminderWithGuidKey =\n        inherit IGrainWithGuidKey\n        /// Receives a reminder and processes it asynchronously.\n        abstract member ReceiveReminderAsync: reminder: ReminderDto -> Task<Result<unit, GraceError>>\n        /// Schedules a reminder to be sent to the actor after a specified delay.\n        abstract member ScheduleReminderAsync: reminderType: ReminderTypes -> delay: Duration -> state: ReminderState -> correlationId: CorrelationId -> Task\n\n    /// Defines the operations that an actor must implement to handle Grace reminders.\n    [<Interface>]\n    type IGraceReminderWithStringKey =\n        inherit IGrainWithStringKey\n        /// Receives a reminder and processes it asynchronously.\n        abstract member ReceiveReminderAsync: reminder: ReminderDto -> Task<Result<unit, GraceError>>\n        /// Schedules a reminder to be sent to the actor after a specified delay.\n        abstract member ScheduleReminderAsync: reminderType: ReminderTypes -> delay: Duration -> state: ReminderState -> correlationId: CorrelationId -> Task\n\n    /// Defines the operations for the Branch actor.\n    [<Interface>]\n    type IBranchActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Validates that a branch with this BranchId exists.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Retrieves the current state of the branch.\n        abstract member Get: correlationId: CorrelationId -> Task<BranchDto>\n\n        /// Retrieves the list of events handled by this branch.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<BranchEvent>>\n\n        /// Retrieves the most recent commit from this branch.\n        abstract member GetLatestCommit: correlationId: CorrelationId -> Task<ReferenceDto>\n\n        /// Retrieves the most recent promotion from this branch.\n        abstract member GetLatestPromotion: correlationId: CorrelationId -> Task<ReferenceDto>\n\n        /// Retrieves the parent branch for a given branch.\n        abstract member GetParentBranch: correlationId: CorrelationId -> Task<BranchDto>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: BranchCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n        /// Returns true if this branch has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n        /// Marks the branch as needing to recompute its latest references.\n        abstract member MarkForRecompute: correlationId: CorrelationId -> Task\n\n    /// Defines the operations for the BranchName actor.\n    [<Interface>]\n    type IBranchNameActor =\n        inherit IGrainWithStringKey\n\n        /// Returns the BranchId for the given BranchName.\n        abstract member GetBranchId: correlationId: CorrelationId -> Task<BranchId option>\n\n        /// Sets the BranchId that matches the BranchName.\n        abstract member SetBranchId: branchId: BranchId -> correlationId: CorrelationId -> Task\n\n    /// Defines the operations for the Diff actor.\n    [<Interface>]\n    type IDiffActor =\n        inherit IGraceReminderWithStringKey\n\n        /// Populates the contents of the diff without returning the results.\n        abstract member Compute: correlationId: CorrelationId -> Task<GraceResult<string>>\n\n        /// Gets the results of the diff. If the diff has not already been computed, it will be computed.\n        abstract member GetDiff: correlationId: CorrelationId -> Task<DiffDto>\n\n    /// Defines the operations for the DirectoryAppearance actor.\n    [<Interface>]\n    type IDirectoryAppearanceActor =\n        inherit IGrainWithGuidKey\n\n        /// Adds an appearance to the directory appearance list.\n        abstract member Add: appearance: Appearance -> correlationId: CorrelationId -> Task\n\n        /// Removes an appearance from the directory appearance list.\n        abstract member Remove: appearance: Appearance -> correlationId: CorrelationId -> Task\n\n        /// Checks if the directory appearance list contains the given appearance.\n        abstract member Contains: appearance: Appearance -> correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the sorted set of appearances for this directory.\n        abstract member Appearances: correlationId: CorrelationId -> Task<SortedSet<Appearance>>\n\n    ///Defines the operations for the DirectoryVersion actor.\n    [<Interface>]\n    type IDirectoryVersionActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Returns true if the actor instance already exists.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the DirectoryVersion instance for this directory.\n        abstract member Get: correlationId: CorrelationId -> Task<DirectoryVersionDto>\n\n        /// Returns the list of subdirectories contained in this directory.\n        abstract member GetCreatedAt: correlationId: CorrelationId -> Task<Instant>\n\n        /// Returns the list of subdirectories contained in this directory.\n        abstract member GetDirectories: correlationId: CorrelationId -> Task<List<DirectoryVersionId>>\n\n        /// Returns the list of files contained in this directory.\n        abstract member GetFiles: correlationId: CorrelationId -> Task<List<FileVersion>>\n\n        /// Returns the Sha256 hash value for this directory.\n        abstract member GetSha256Hash: correlationId: CorrelationId -> Task<Sha256Hash>\n\n        /// Returns the total size of files contained in this directory. This does not include files in subdirectories; for that, use GetSizeRecursive().\n        abstract member GetSize: correlationId: CorrelationId -> Task<int64>\n\n        /// Returns a list of DirectoryVersion objects for all subdirectories.\n        abstract member GetRecursiveDirectoryVersions: forceRegenerate: bool -> correlationId: CorrelationId -> Task<DirectoryVersionDto array>\n\n        /// Returns the total size of files contained in this directory and all subdirectories.\n        abstract member GetRecursiveSize: correlationId: CorrelationId -> Task<int64>\n\n        /// Returns the Uri, with a shared access signature, to download the .zip file containing the contents of this directory and all subdirectories. If the .zip file doesn't exist, it will be created.\n        abstract member GetZipFileUri: correlationId: CorrelationId -> Task<UriWithSharedAccessSignature>\n\n        /// Delete the DirectoryVersion and all subdirectories and files.\n        abstract member Delete: correlationId: CorrelationId -> Task<GraceResult<string>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: DirectoryVersionCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the FileAppearance actor.\n    [<Interface>]\n    type IFileAppearanceActor =\n        inherit IGrainWithStringKey\n\n        /// Adds an appearance to the directory appearance list.\n        abstract member Add: appearance: Appearance -> correlationId: CorrelationId -> Task\n\n        /// Removes an appearance from the directory appearance list.\n        abstract member Remove: appearance: Appearance -> correlationId: CorrelationId -> Task\n\n        /// Checks if the directory appearance list contains the given appearance.\n        abstract member Contains: appearance: Appearance -> correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the sorted set of appearances for this directory.\n        abstract member Appearances: correlationId: CorrelationId -> Task<SortedSet<Appearance>>\n\n    /// Defines the operations for the ReminderServiceLock actor.\n    [<Interface>]\n    type IGlobalLockActor =\n        inherit IGrainWithStringKey\n\n        /// Attempts to acquire a global lock for the Reminder Service. Returns true if the lock was acquired, otherwise false.\n        abstract member AcquireLock: lockedBy: string -> Task<bool>\n\n        /// Releases the global lock for the Reminder Service.\n        abstract member ReleaseLock: releasedBy: string -> Task<Result<unit, string>>\n\n        /// Returns true if the lock is currently held by any instance.\n        abstract member IsLocked: unit -> Task<bool>\n\n    ///Defines the operations for the Organization actor.\n    [<Interface>]\n    type IOrganizationActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Returns true if an organization with this ActorId already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if an organization with this ActorId has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if an repository with this name exists for this owner.\n        abstract member RepositoryExists: repositoryName: RepositoryName -> correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current state of the organization.\n        abstract member Get: correlationId: CorrelationId -> Task<OrganizationDto>\n\n        /// Returns a list of the repositories under this organization.\n        abstract member ListRepositories: correlationId: CorrelationId -> Task<IReadOnlyDictionary<RepositoryId, RepositoryName>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: OrganizationCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the OrganizationName actor.\n    [<Interface>]\n    type IOrganizationNameActor =\n        inherit IGrainWithStringKey\n\n        /// Returns true if an organization with this organization name already exists in the database.\n        abstract member SetOrganizationId: organizationId: OrganizationId -> correlationId: CorrelationId -> Task\n\n        /// Returns the OrganizationId for the given OrganizationName.\n        abstract member GetOrganizationId: correlationId: CorrelationId -> Task<OrganizationId option>\n\n    /// Defines the operations for the Owner actor.\n    [<Interface>]\n    type IOwnerActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Returns true if an owner with this ActorId already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if an owner with this ActorId has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if an organization with this name exists for this owner.\n        abstract member OrganizationExists: organizationName: string -> correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current state of the owner.\n        abstract member Get: correlationId: CorrelationId -> Task<OwnerDto>\n\n        /// Returns a list of the organizations under this owner.\n        abstract member ListOrganizations: correlationId: CorrelationId -> Task<IReadOnlyDictionary<OrganizationId, OrganizationName>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: OwnerCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations fpr the OwnerName actor.\n    [<Interface>]\n    type IOwnerNameActor =\n        inherit IGrainWithStringKey\n        /// Clears the OwnerId for the given OwnerName.\n        abstract member ClearOwnerId: correlationId: CorrelationId -> Task\n\n        /// Returns the OwnerId for the given OwnerName.\n        abstract member GetOwnerId: correlationId: CorrelationId -> Task<OwnerId option>\n\n        /// Sets the OwnerId for a given OwnerName.\n        abstract member SetOwnerId: ownerId: OwnerId -> correlationId: CorrelationId -> Task\n\n    /// Defines the operations for the Reference actor.\n    [<Interface>]\n    type IReferenceActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Returns true if the reference already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the dto for this reference.\n        abstract member Get: correlationId: CorrelationId -> Task<ReferenceDto>\n\n        /// Returns the ReferenceType for this reference.\n        abstract member GetReferenceType: correlationId: CorrelationId -> Task<ReferenceType>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: ReferenceCommand -> eventMetadata: EventMetadata -> Task<GraceResult<ReferenceDto>>\n\n        /// Returns true if the reference has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n    [<Interface>]\n    type IReminderActor =\n        inherit IGrainWithGuidKey\n\n        /// Creates a new reminder in the database.\n        abstract member Create: reminder: ReminderDto -> correlationId: CorrelationId -> Task\n\n        /// Deletes the reminder from the database.\n        abstract member Delete: correlationId: CorrelationId -> Task\n\n        /// Returns true if the reminder exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the reminder from the database.\n        abstract member Get: correlationId: CorrelationId -> Task<ReminderDto>\n\n        /// Sends the reminder to the source actor.\n        abstract member Remind: correlationId: CorrelationId -> Task<Result<unit, GraceError>>\n\n    /// Defines the operations for the PromotionSet actor.\n    [<Interface>]\n    type IPromotionSetActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Returns true if this promotion set already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if this promotion set has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current state of the promotion set.\n        abstract member Get: correlationId: CorrelationId -> Task<PromotionSetDto>\n\n        /// Returns the list of events handled by this promotion set.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<PromotionSetEvent>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: PromotionSetCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the Policy actor.\n    [<Interface>]\n    type IPolicyActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns the current policy snapshot.\n        abstract member GetCurrent: correlationId: CorrelationId -> Task<PolicySnapshot option>\n\n        /// Returns all policy snapshots.\n        abstract member GetSnapshots: correlationId: CorrelationId -> Task<IReadOnlyList<PolicySnapshot>>\n\n        /// Returns policy acknowledgements.\n        abstract member GetAcknowledgements: correlationId: CorrelationId -> Task<IReadOnlyList<PolicyAcknowledgement>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: PolicyCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the Review actor.\n    [<Interface>]\n    type IReviewActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns the current review notes.\n        abstract member GetNotes: correlationId: CorrelationId -> Task<ReviewNotes option>\n\n        /// Returns checkpoints for this review target.\n        abstract member GetCheckpoints: correlationId: CorrelationId -> Task<IReadOnlyList<ReviewCheckpoint>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: ReviewCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the PromotionQueue actor.\n    [<Interface>]\n    type IPromotionQueueActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns true if this promotion queue already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current state of the promotion queue.\n        abstract member Get: correlationId: CorrelationId -> Task<PromotionQueue>\n\n        /// Returns the list of events handled by this promotion queue.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<PromotionQueueEvent>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: PromotionQueueCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the ValidationSet actor.\n    [<Interface>]\n    type IValidationSetActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns true if this validation set already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if this validation set has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current validation set.\n        abstract member Get: correlationId: CorrelationId -> Task<ValidationSetDto option>\n\n        /// Returns the list of events handled by this validation set.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<ValidationSetEvent>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: ValidationSetCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the ValidationResult actor.\n    [<Interface>]\n    type IValidationResultActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns true if this validation result already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current validation result.\n        abstract member Get: correlationId: CorrelationId -> Task<ValidationResultDto option>\n\n        /// Returns the list of events handled by this validation result.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<ValidationResultEvent>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: ValidationResultCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the Artifact actor.\n    [<Interface>]\n    type IArtifactActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns true if this artifact already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current artifact metadata.\n        abstract member Get: correlationId: CorrelationId -> Task<ArtifactMetadata option>\n\n        /// Returns the list of events handled by this artifact.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<ArtifactEvent>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: ArtifactCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the WorkItemNumber actor.\n    [<Interface>]\n    type IWorkItemNumberActor =\n        inherit IGrainWithStringKey\n\n        /// Returns the WorkItemId for a repository-scoped WorkItemNumber.\n        abstract member GetWorkItemId: workItemNumber: WorkItemNumber -> correlationId: CorrelationId -> Task<WorkItemId option>\n\n        /// Caches a WorkItemNumber -> WorkItemId mapping while the actor is active.\n        abstract member SetWorkItemId: workItemNumber: WorkItemNumber -> workItemId: WorkItemId -> correlationId: CorrelationId -> Task\n\n    /// Defines the operations for the WorkItemNumberCounter actor.\n    [<Interface>]\n    type IWorkItemNumberCounterActor =\n        inherit IGrainWithStringKey\n\n        /// Allocates and persists the next WorkItemNumber for a repository.\n        abstract member AllocateNext: correlationId: CorrelationId -> Task<WorkItemNumber>\n\n    /// Defines the operations for the WorkItem actor.\n    [<Interface>]\n    type IWorkItemActor =\n        inherit IGrainWithGuidKey\n\n        /// Returns true if this work item already exists in the database.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns the current state of the work item.\n        abstract member Get: correlationId: CorrelationId -> Task<WorkItemDto>\n\n        /// Returns the list of events handled by this work item.\n        abstract member GetEvents: correlationId: CorrelationId -> Task<IReadOnlyList<WorkItemEvent>>\n\n        /// Validates incoming commands and converts them to events that are stored in the database.\n        abstract member Handle: command: WorkItemCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the Repository actor.\n    [<Interface>]\n    type IRepositoryActor =\n        inherit IGraceReminderWithGuidKey\n\n        /// Returns true if this actor already exists in the database, otherwise false.\n        abstract member Exists: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if the repository has been created but is empty; otherwise false.\n        abstract member IsEmpty: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns true if this repository has been deleted.\n        abstract member IsDeleted: correlationId: CorrelationId -> Task<bool>\n\n        /// Returns a record with the current state of the repository.\n        abstract member Get: correlationId: CorrelationId -> Task<RepositoryDto>\n\n        /// Returns the object storage provider for this repository.\n        abstract member GetObjectStorageProvider: correlationId: CorrelationId -> Task<ObjectStorageProvider>\n\n        /// Processes commands by checking that they're valid, and then converting them into events.\n        abstract member Handle: command: RepositoryCommand -> eventMetadata: EventMetadata -> Task<GraceResult<string>>\n\n    /// Defines the operations for the AccessControl actor.\n    [<Interface>]\n    type IAccessControlActor =\n        inherit IGrainWithStringKey\n\n        /// Handles role assignment commands.\n        abstract member Handle: command: AccessControlCommand -> eventMetadata: EventMetadata -> Task<GraceResult<RoleAssignment list>>\n\n        /// Returns role assignments for this scope.\n        abstract member GetAssignments: principal: Principal option -> correlationId: CorrelationId -> Task<RoleAssignment list>\n\n    /// Defines the operations for the RepositoryPermission actor.\n    [<Interface>]\n    type IRepositoryPermissionActor =\n        inherit IGrainWithStringKey\n\n        /// Handles repository path permission commands.\n        abstract member Handle: command: RepositoryPermissionCommand -> eventMetadata: EventMetadata -> Task<GraceResult<PathPermission list>>\n\n        /// Returns path permissions for this repository.\n        abstract member GetPathPermissions: pathFilter: RelativePath option -> correlationId: CorrelationId -> Task<PathPermission list>\n\n    /// Defines the operations for the RepositoryName actor.\n    [<Interface>]\n    type IGrainRepositoryActor =\n        inherit IGrainWithStringKey\n\n        /// Sets the RepositoryId that matches the RepositoryName.\n        abstract member SetRepositoryId: repositoryId: RepositoryId -> correlationId: CorrelationId -> Task\n\n        /// Returns the RepositoryId for the given RepositoryName.\n        abstract member GetRepositoryId: correlationId: CorrelationId -> Task<RepositoryId option>\n\n    /// Defines the operations for the RepositoryName actor.\n    [<Interface>]\n    type IRepositoryNameActor =\n        inherit IGrainWithStringKey\n\n        /// Sets the RepositoryId that matches the RepositoryName.\n        abstract member SetRepositoryId: repositoryId: RepositoryId -> correlationId: CorrelationId -> Task\n\n        /// Returns the RepositoryId for the given RepositoryName.\n        abstract member GetRepositoryId: correlationId: CorrelationId -> Task<RepositoryId option>\n\n    /// Defines the operations for the PersonalAccessToken actor.\n    [<Interface>]\n    type IPersonalAccessTokenActor =\n        inherit IGrainWithStringKey\n\n        abstract member CreateToken:\n            name: string ->\n            claims: string list ->\n            groupIds: string list ->\n            expiresAt: Instant option ->\n            now: Instant ->\n            correlationId: CorrelationId ->\n                Task<Result<PersonalAccessTokenCreated, GraceError>>\n\n        abstract member ListTokens:\n            includeRevoked: bool -> includeExpired: bool -> now: Instant -> correlationId: CorrelationId -> Task<PersonalAccessTokenSummary list>\n\n        abstract member RevokeToken:\n            tokenId: PersonalAccessTokenId -> now: Instant -> correlationId: CorrelationId -> Task<Result<PersonalAccessTokenSummary, GraceError>>\n\n        abstract member ValidateToken:\n            tokenId: PersonalAccessTokenId ->\n            secret: byte [] ->\n            now: Instant ->\n            correlationId: CorrelationId ->\n                Task<PersonalAccessTokenValidationResult option>\n"
  },
  {
    "path": "src/Grace.Actors/NamedSection.Actor.fs",
    "content": "﻿namespace Grace.Actors\n\nopen Grace.Actors.Constants\n\nmodule NamedSection =\n\n    let ActorName = ActorName.NamedSection\n"
  },
  {
    "path": "src/Grace.Actors/Organization.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Events\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.Organization\nopen Grace.Types.Types\nopen Grace.Shared.Validation.Errors\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Linq\nopen System.Runtime.Serialization\nopen System.Text.Json\nopen System.Threading.Tasks\n\nmodule Organization =\n\n    type OrganizationActor([<PersistentState(StateName.Organization, Constants.GraceActorStorage)>] state: IPersistentState<List<OrganizationEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Organization\n\n        let log = loggerFactory.CreateLogger(\"Organization.Actor\")\n\n        let mutable organizationDto = OrganizationDto.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            organizationDto <-\n                state.State\n                |> Seq.fold (fun organizationDto event -> OrganizationDto.UpdateDto event organizationDto) organizationDto\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent organizationEvent =\n            task {\n                try\n                    state.State.Add(organizationEvent)\n\n                    do! state.WriteStateAsync()\n\n                    // Update the Dto based on the current event.\n                    organizationDto <-\n                        organizationDto\n                        |> OrganizationDto.UpdateDto organizationEvent\n\n                    // Publish the event to the rest of the world.\n                    let graceEvent = OrganizationEvent organizationEvent\n                    do! publishGraceEvent graceEvent organizationEvent.Metadata\n\n                    let returnValue =\n                        (GraceReturnValue.Create \"Organization command succeeded.\" organizationEvent.Metadata.CorrelationId)\n                            .enhance(nameof OwnerId, organizationDto.OwnerId)\n                            .enhance(nameof OrganizationId, organizationDto.OrganizationId)\n                            .enhance(nameof OrganizationName, organizationDto.OrganizationName)\n                            .enhance (nameof OrganizationEventType, getDiscriminatedUnionFullName organizationEvent.Event)\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    let exceptionResponse = ExceptionResponse.Create ex\n\n                    let graceError =\n                        GraceError.Create\n                            (OrganizationError.getErrorMessage OrganizationError.FailedWhileApplyingEvent)\n                            organizationEvent.Metadata.CorrelationId\n\n                    graceError\n                        .enhance(\n                            \"Exception details\",\n                            exceptionResponse.``exception``\n                            + exceptionResponse.innerException\n                        )\n                        .enhance(nameof OrganizationId, organizationDto.OrganizationId)\n                        .enhance(nameof OrganizationName, organizationDto.OrganizationName)\n                        .enhance (nameof OrganizationEventType, getDiscriminatedUnionFullName organizationEvent.Event)\n                    |> ignore\n\n                    return Error graceError\n            }\n\n        /// Deletes all of the repositories provided, by sending a DeleteLogical command to each one.\n        member private this.LogicalDeleteRepositories(repositories: RepositoryDto array, metadata: EventMetadata, deleteReason: DeleteReason) =\n            task {\n                let results = ConcurrentQueue<GraceResult<string>>()\n\n                // Loop through each repository and send a DeleteLogical command to it.\n                do!\n                    Parallel.ForEachAsync(\n                        repositories,\n                        Constants.ParallelOptions,\n                        (fun repository ct ->\n                            ValueTask(\n                                task {\n                                    if repository.DeletedAt |> Option.isNone then\n                                        let repositoryActor =\n                                            Repository.CreateActorProxy repository.OrganizationId repository.RepositoryId metadata.CorrelationId\n\n                                        let! result =\n                                            repositoryActor.Handle\n                                                (RepositoryCommand.DeleteLogical(\n                                                    true,\n                                                    $\"Cascaded from deleting organization. ownerId: {organizationDto.OwnerId}; organizationId: {organizationDto.OrganizationId}; organizationName: {organizationDto.OrganizationName}; deleteReason: {deleteReason}\"\n                                                ))\n                                                metadata\n\n                                        results.Enqueue(result)\n                                }\n                            ))\n                    )\n\n                // Check if any of the commands failed, and if so, return the first error.\n                let overallResult =\n                    results\n                    |> Seq.tryPick (fun result ->\n                        match result with\n                        | Ok _ -> None\n                        | Error error -> Some(error))\n\n                match overallResult with\n                | None -> return Ok()\n                | Some error -> return Error error\n            }\n\n        interface IGraceReminderWithGuidKey with\n            /// Schedules a Grace reminder.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminder =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            organizationDto.OwnerId\n                            organizationDto.OrganizationId\n                            Guid.Empty\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminder\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    this.correlationId <- reminder.CorrelationId\n\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.PhysicalDeletion, ReminderState.OrganizationPhysicalDeletion physicalDeletionReminderState ->\n                        this.correlationId <- physicalDeletionReminderState.CorrelationId\n\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Deleted physical state for organization; OrganizationId: {organizationId}; OrganizationName: {organizationName}; OwnerId: {ownerId}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            physicalDeletionReminderState.CorrelationId,\n                            organizationDto.OrganizationId,\n                            organizationDto.OrganizationName,\n                            organizationDto.OwnerId,\n                            physicalDeletionReminderState.DeleteReason\n                        )\n\n                        // Deactivate the actor after the PhysicalDeletion reminder is processed.\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId\n                            )\n                }\n\n        interface IOrganizationActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n                Task.FromResult(if organizationDto.UpdatedAt.IsSome then true else false)\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                Task.FromResult(if organizationDto.DeletedAt.IsSome then true else false)\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                Task.FromResult(organizationDto)\n\n            member this.RepositoryExists repositoryName correlationId =\n                task {\n                    this.correlationId <- correlationId\n                    let actorProxy = RepositoryName.CreateActorProxy organizationDto.OwnerId organizationDto.OrganizationId repositoryName correlationId\n\n                    match! actorProxy.GetRepositoryId(correlationId) with\n                    | Some repositoryId -> return true\n                    | None -> return false\n                }\n\n            member this.ListRepositories correlationId =\n                task {\n                    this.correlationId <- correlationId\n                    let! repositoryDtos = Services.getRepositories organizationDto.OwnerId organizationDto.OrganizationId Int32.MaxValue false\n                    let dict = repositoryDtos.ToDictionary((fun repo -> repo.RepositoryId), (fun repo -> repo.RepositoryName))\n\n                    return dict :> IReadOnlyDictionary<RepositoryId, RepositoryName>\n                }\n            //Task.FromResult(organizationDto.Repositories :> IReadOnlyDictionary<RepositoryId, RepositoryName>)\n\n            member this.Handle (command: OrganizationCommand) metadata =\n                let isValid (command: OrganizationCommand) (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (getErrorMessage OrganizationError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | OrganizationCommand.Create (organizationId, organizationName, ownerId) ->\n                                match organizationDto.UpdatedAt with\n                                | Some _ ->\n                                    return Error(GraceError.Create (OrganizationError.getErrorMessage OrganizationIdAlreadyExists) metadata.CorrelationId)\n                                | None -> return Ok command\n                            | _ ->\n                                match organizationDto.UpdatedAt with\n                                | Some _ -> return Ok command\n                                | None -> return Error(GraceError.Create (OrganizationError.getErrorMessage OrganizationIdDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let processCommand (command: OrganizationCommand) (metadata: EventMetadata) =\n                    task {\n                        try\n                            let! eventResult =\n                                task {\n                                    match command with\n                                    | OrganizationCommand.Create (organizationId, organizationName, ownerId) ->\n                                        return Ok(OrganizationEventType.Created(organizationId, organizationName, ownerId))\n                                    | OrganizationCommand.SetName (organizationName) -> return Ok(OrganizationEventType.NameSet(organizationName))\n                                    | OrganizationCommand.SetType (organizationType) -> return Ok(OrganizationEventType.TypeSet(organizationType))\n                                    | OrganizationCommand.SetSearchVisibility (searchVisibility) ->\n                                        return Ok(OrganizationEventType.SearchVisibilitySet(searchVisibility))\n                                    | OrganizationCommand.SetDescription (description) -> return Ok(OrganizationEventType.DescriptionSet(description))\n                                    | OrganizationCommand.DeleteLogical (force, deleteReason) ->\n                                        // Get the list of branches that aren't already deleted.\n                                        let! repositories = getRepositories organizationDto.OwnerId organizationDto.OrganizationId Int32.MaxValue false\n\n                                        // If the organization contains repositories, and any of them isn't already deleted, and the force flag is not set, return an error.\n                                        if not <| force\n                                           && repositories.Length > 0\n                                           && repositories.Any(fun repository -> repository.DeletedAt |> Option.isNone) then\n                                            let metadataObj =\n                                                Dictionary<string, obj>(metadata.Properties.Select(fun kvp -> KeyValuePair<string, obj>(kvp.Key, kvp.Value)))\n\n                                            return\n                                                Error(\n                                                    GraceError.CreateWithMetadata\n                                                        null\n                                                        (OrganizationError.getErrorMessage OrganizationContainsRepositories)\n                                                        metadata.CorrelationId\n                                                        metadataObj\n                                                )\n                                        else\n                                            // Delete the repositories.\n                                            match! this.LogicalDeleteRepositories(repositories, metadata, deleteReason) with\n                                            | Ok _ ->\n                                                let (physicalDeletionReminderState: PhysicalDeletionReminderState) =\n                                                    { DeleteReason = deleteReason; CorrelationId = metadata.CorrelationId }\n\n                                                do!\n                                                    (this :> IGraceReminderWithGuidKey)\n                                                        .ScheduleReminderAsync\n                                                        ReminderTypes.PhysicalDeletion\n                                                        DefaultPhysicalDeletionReminderDuration\n                                                        (ReminderState.OrganizationPhysicalDeletion physicalDeletionReminderState)\n                                                        metadata.CorrelationId\n\n                                                return Ok(LogicalDeleted(force, deleteReason))\n                                            | Error error -> return Error error\n                                    | OrganizationCommand.DeletePhysical ->\n                                        // Delete saved state for this actor.\n                                        do! state.ClearStateAsync()\n\n                                        // Deactivate the actor after the PhysicalDeletion is processed.\n                                        this.DeactivateOnIdle()\n\n                                        return Ok OrganizationEventType.PhysicalDeleted\n                                    | OrganizationCommand.Undelete -> return Ok OrganizationEventType.Undeleted\n                                }\n\n                            match eventResult with\n                            | Ok event -> return! this.ApplyEvent { Event = event; Metadata = metadata }\n                            | Error error -> return Error error\n                        with\n                        | ex ->\n                            let metadataObj = Dictionary<string, obj>(metadata.Properties.Select(fun kvp -> KeyValuePair<string, obj>(kvp.Key, kvp.Value)))\n                            return Error(GraceError.CreateWithMetadata ex String.Empty metadata.CorrelationId metadataObj)\n                    }\n\n                task {\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/OrganizationName.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule OrganizationName =\n\n    type OrganizationNameActor() =\n        inherit Grain()\n\n        static let actorName = ActorName.OrganizationName\n\n        let log = loggerFactory.CreateLogger(\"OrganizationName.Actor\")\n\n        let mutable cachedOrganizationId: OrganizationId option = None\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n            //let idSections = this.GetGrainId().Key.ToString().Split('|')\n            //let organizationName = idSections[0]\n            //let ownerId = idSections[1]\n\n            logActorActivation log this.IdentityString activateStartTime \"In-memory only\"\n\n            Task.CompletedTask\n\n        interface IOrganizationNameActor with\n            member this.GetOrganizationId correlationId =\n                this.correlationId <- correlationId\n                cachedOrganizationId |> returnTask\n\n            member this.SetOrganizationId (organizationId: OrganizationId) correlationId =\n                this.correlationId <- correlationId\n\n                if organizationId <> Guid.Empty then cachedOrganizationId <- Some organizationId\n\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/Owner.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen FSharp.Control\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Actors.Interfaces\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types\nopen Grace.Types.Organization\nopen Grace.Types.Owner\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Linq\nopen System.Runtime.Serialization\nopen System.Text.Json\nopen System.Threading.Tasks\n\nmodule Owner =\n\n    type OwnerActor([<PersistentState(StateName.Owner, Constants.GraceActorStorage)>] state: IPersistentState<List<OwnerEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Owner\n\n        let log = loggerFactory.CreateLogger(\"Owner.Actor\")\n\n        let mutable ownerDto = OwnerDto.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            ownerDto <-\n                state.State\n                |> Seq.fold (fun ownerDto ownerEvent -> OwnerDto.UpdateDto ownerEvent ownerDto) OwnerDto.Default\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent ownerEvent =\n            task {\n                try\n                    state.State.Add(ownerEvent)\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Owner.Actor writing state. CorrelationId: {CorrelationId}; OwnerId: {OwnerId}.\",\n                        getCurrentInstantExtended (),\n                        ownerEvent.Metadata.CorrelationId,\n                        ownerDto.OwnerId\n                    )\n\n                    do! state.WriteStateAsync()\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Owner.Actor state write completed. CorrelationId: {CorrelationId}; OwnerId: {OwnerId}.\",\n                        getCurrentInstantExtended (),\n                        ownerEvent.Metadata.CorrelationId,\n                        ownerDto.OwnerId\n                    )\n\n                    // Update the Dto based on the current event.\n                    ownerDto <- ownerDto |> OwnerDto.UpdateDto ownerEvent\n\n                    // Publish the event to the rest of the world.\n                    let graceEvent = Events.GraceEvent.OwnerEvent ownerEvent\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Owner.Actor publishing GraceEvent. CorrelationId: {CorrelationId}; OwnerId: {OwnerId}.\",\n                        getCurrentInstantExtended (),\n                        ownerEvent.Metadata.CorrelationId,\n                        ownerDto.OwnerId\n                    )\n\n                    do! publishGraceEvent graceEvent ownerEvent.Metadata\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Owner.Actor published GraceEvent. CorrelationId: {CorrelationId}; OwnerId: {OwnerId}.\",\n                        getCurrentInstantExtended (),\n                        ownerEvent.Metadata.CorrelationId,\n                        ownerDto.OwnerId\n                    )\n\n                    let returnValue = GraceReturnValue.Create \"Owner command succeeded.\" ownerEvent.Metadata.CorrelationId\n\n                    returnValue\n                        .enhance(nameof OwnerId, ownerDto.OwnerId)\n                        .enhance(nameof OwnerName, ownerDto.OwnerName)\n                        .enhance (nameof OwnerEventType, getDiscriminatedUnionFullName ownerEvent.Event)\n                    |> ignore\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    let exceptionResponse = ExceptionResponse.Create ex\n                    log.LogError(ex, \"Exception in Owner.Actor: event: {event}\", (serialize ownerEvent))\n                    log.LogError(\"Exception details: {exception}\", serialize exceptionResponse)\n                    let graceError = GraceError.Create (getErrorMessage OwnerError.FailedWhileApplyingEvent) ownerEvent.Metadata.CorrelationId\n\n                    graceError\n                        .enhance(\n                            \"Exception details\",\n                            exceptionResponse.``exception``\n                            + exceptionResponse.innerException\n                        )\n                        .enhance(nameof OwnerId, ownerDto.OwnerId)\n                        .enhance(nameof OwnerName, ownerDto.OwnerName)\n                        .enhance (nameof OwnerEventType, getDiscriminatedUnionFullName ownerEvent.Event)\n                    |> ignore\n\n                    return Error graceError\n            }\n\n        /// Sends a DeleteLogical command to each organization provided.\n        member private this.LogicalDeleteOrganizations(organizations: OrganizationDto array, metadata: EventMetadata, deleteReason: DeleteReason) =\n            // Loop through the orgs, sending a DeleteLogical command to each. If any of them fail, return the first error.\n            task {\n                let results = ConcurrentQueue<GraceResult<string>>()\n\n                // Loop through each organization and send a DeleteLogical command to it.\n                do!\n                    Parallel.ForEachAsync(\n                        organizations,\n                        Constants.ParallelOptions,\n                        (fun organization ct ->\n                            ValueTask(\n                                task {\n                                    if organization.DeletedAt |> Option.isNone then\n                                        let organizationActor = Organization.CreateActorProxy organization.OrganizationId metadata.CorrelationId\n\n                                        let! result =\n                                            organizationActor.Handle\n                                                (Organization.DeleteLogical(\n                                                    true,\n                                                    $\"Cascaded from deleting owner. ownerId: {ownerDto.OwnerId}; ownerName: {ownerDto.OwnerName}; deleteReason: {deleteReason}\"\n                                                ))\n                                                metadata\n\n                                        results.Enqueue(result)\n                                }\n                            ))\n                    )\n\n                // Check if any of the results were errors. If so, return the first one.\n                let overallResult =\n                    results\n                    |> Seq.tryPick (fun result ->\n                        match result with\n                        | Ok _ -> None\n                        | Error error -> Some(error))\n\n                match overallResult with\n                | None -> return Ok()\n                | Some error -> return Error error\n            }\n\n        interface IGraceReminderWithGuidKey with\n            /// Schedules a Grace reminder.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminder =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            ownerDto.OwnerId\n                            Guid.Empty\n                            Guid.Empty\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminder\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    this.correlationId <- reminder.CorrelationId\n\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.PhysicalDeletion, ReminderState.OwnerPhysicalDeletion physicalDeletionReminderState ->\n                        this.correlationId <- physicalDeletionReminderState.CorrelationId\n\n                        // Delete saved state for this actor.\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Deleted physical state for owner; OwnerId: {ownerId}; OwnerName: {ownerName}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            physicalDeletionReminderState.CorrelationId,\n                            ownerDto.OwnerId,\n                            ownerDto.OwnerName,\n                            physicalDeletionReminderState.DeleteReason\n                        )\n\n                        // Deactivate the actor after the PhysicalDeletion reminder is processed.\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId\n                            )\n                }\n\n        interface IOwnerActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n                ownerDto.UpdatedAt.IsSome |> returnTask\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                ownerDto.DeletedAt.IsSome |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                ownerDto |> returnTask\n\n            member this.OrganizationExists organizationName correlationId =\n                task {\n                    this.correlationId <- correlationId\n                    let actorProxy = OrganizationName.CreateActorProxy ownerDto.OwnerId organizationName correlationId\n\n                    match! actorProxy.GetOrganizationId(correlationId) with\n                    | Some organizationId -> return true\n                    | None -> return false\n                }\n\n            member this.ListOrganizations correlationId =\n                task {\n                    this.correlationId <- correlationId\n                    let! organizationDtos = Services.getOrganizations ownerDto.OwnerId Int32.MaxValue false\n                    let dict = organizationDtos.ToDictionary((fun org -> org.OrganizationId), (fun org -> org.OrganizationName))\n\n                    return dict :> IReadOnlyDictionary<OrganizationId, OrganizationName>\n                }\n\n            member this.Handle command metadata =\n                let isValid command (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (getErrorMessage OwnerError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | OwnerCommand.Create (_, _) ->\n                                match ownerDto.UpdatedAt with\n                                | Some _ -> return Error(GraceError.Create (getErrorMessage OwnerError.OwnerIdAlreadyExists) metadata.CorrelationId)\n                                | None -> return Ok command\n                            | _ ->\n                                match ownerDto.UpdatedAt with\n                                | Some _ -> return Ok command\n                                | None -> return Error(GraceError.Create (getErrorMessage OwnerError.OwnerIdDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let processCommand (command: OwnerCommand) (metadata: EventMetadata) =\n                    task {\n                        try\n                            let! eventResult =\n                                task {\n                                    match command with\n                                    | OwnerCommand.Create (ownerId, ownerName) -> return Ok(OwnerEventType.Created(ownerId, ownerName))\n                                    | OwnerCommand.SetName newName ->\n                                        // Clear the OwnerNameActor for the old name.\n                                        let ownerNameActor = OwnerName.CreateActorProxy ownerDto.OwnerName metadata.CorrelationId\n                                        do! ownerNameActor.ClearOwnerId metadata.CorrelationId\n                                        memoryCache.RemoveOwnerNameEntry ownerDto.OwnerName\n\n                                        // Set the OwnerNameActor for the new name.\n                                        let ownerNameActor = OwnerName.CreateActorProxy ownerDto.OwnerName metadata.CorrelationId\n                                        do! ownerNameActor.SetOwnerId ownerDto.OwnerId metadata.CorrelationId\n                                        memoryCache.CreateOwnerNameEntry newName ownerDto.OwnerId\n\n                                        return Ok(OwnerEventType.NameSet newName)\n                                    | OwnerCommand.SetType ownerType -> return Ok(OwnerEventType.TypeSet ownerType)\n                                    | OwnerCommand.SetSearchVisibility searchVisibility -> return Ok(OwnerEventType.SearchVisibilitySet searchVisibility)\n                                    | OwnerCommand.SetDescription description -> return Ok(OwnerEventType.DescriptionSet description)\n                                    | OwnerCommand.DeleteLogical (force, deleteReason) ->\n                                        // Get the list of organizations that aren't already deleted.\n                                        let! organizations = getOrganizations ownerDto.OwnerId Int32.MaxValue false\n\n                                        // If the owner contains active organizations, and the force flag is not set, return an error.\n                                        if not <| force\n                                           && organizations.Length > 0\n                                           && organizations.Any(fun organization -> organization.DeletedAt |> Option.isNone) then\n                                            let metadataObj =\n                                                Dictionary<string, obj>(metadata.Properties.Select(fun kvp -> KeyValuePair<string, obj>(kvp.Key, kvp.Value)))\n\n                                            return\n                                                Error(\n                                                    GraceError.CreateWithMetadata\n                                                        null\n                                                        (OwnerError.getErrorMessage OwnerContainsOrganizations)\n                                                        metadata.CorrelationId\n                                                        metadataObj\n                                                )\n                                        else\n                                            // Delete the organizations.\n                                            match! this.LogicalDeleteOrganizations(organizations, metadata, deleteReason) with\n                                            | Ok _ ->\n                                                let (physicalDeletionReminderState: PhysicalDeletionReminderState) =\n                                                    { DeleteReason = deleteReason; CorrelationId = metadata.CorrelationId }\n\n                                                do!\n                                                    (this :> IGraceReminderWithGuidKey)\n                                                        .ScheduleReminderAsync\n                                                        ReminderTypes.PhysicalDeletion\n                                                        DefaultPhysicalDeletionReminderDuration\n                                                        (ReminderState.OwnerPhysicalDeletion physicalDeletionReminderState)\n                                                        metadata.CorrelationId\n\n                                                return Ok(LogicalDeleted(force, deleteReason))\n                                            | Error error -> return Error error\n                                    | OwnerCommand.DeletePhysical ->\n                                        // Delete saved state for this actor.\n                                        do! state.ClearStateAsync()\n\n                                        // Deactivate the actor after the PhysicalDeletion is processed.\n                                        this.DeactivateOnIdle()\n                                        return Ok(OwnerEventType.PhysicalDeleted)\n                                    | OwnerCommand.Undelete -> return Ok(OwnerEventType.Undeleted)\n                                }\n\n                            match eventResult with\n                            | Ok event ->\n                                //logToConsole $\"In Owner.Actor.Handle(): GraceEvent: {serialize event}; Metadata: {serialize metadata}\"\n                                return! this.ApplyEvent { Event = event; Metadata = metadata }\n                            | Error error -> return Error error\n                        with\n                        | ex ->\n                            let metadataObj = Dictionary<string, obj>(metadata.Properties.Select(fun kvp -> KeyValuePair<string, obj>(kvp.Key, kvp.Value)))\n                            return Error(GraceError.CreateWithMetadata ex String.Empty metadata.CorrelationId metadataObj)\n                    }\n\n                task {\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/OwnerName.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule OwnerName =\n\n    type OwnerNameActor(log: ILogger<OwnerNameActor>) =\n        inherit Grain()\n\n        static let actorName = ActorName.OwnerName\n\n        let log = loggerFactory.CreateLogger(\"OwnerName.Actor\")\n\n        let mutable cachedOwnerId: OwnerId option = None\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime \"In-memory only\"\n\n            Task.CompletedTask\n\n        interface IOwnerNameActor with\n            member this.ClearOwnerId correlationId =\n                this.correlationId <- correlationId\n                cachedOwnerId <- None\n\n                Task.CompletedTask\n\n            member this.GetOwnerId(correlationId) =\n                this.correlationId <- correlationId\n                cachedOwnerId |> returnTask\n\n            member this.SetOwnerId (ownerId: OwnerId) correlationId =\n                this.correlationId <- correlationId\n                cachedOwnerId <- Some ownerId\n\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/PersonalAccessToken.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.PersonalAccessToken\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Security.Cryptography\nopen System.Threading.Tasks\n\nmodule PersonalAccessToken =\n\n    [<GenerateSerializer>]\n    type PersonalAccessTokenRecord =\n        {\n            TokenId: PersonalAccessTokenId\n            Name: string\n            CreatedAt: Instant\n            ExpiresAt: Instant option\n            LastUsedAt: Instant option\n            RevokedAt: Instant option\n            Salt: byte array\n            Hash: byte array\n            Claims: string list\n            GroupIds: string list\n        }\n\n    [<GenerateSerializer>]\n    type PersonalAccessTokenState = { Tokens: PersonalAccessTokenRecord list }\n\n    module PersonalAccessTokenState =\n        let Empty = { Tokens = [] }\n\n    type PersonalAccessTokenActor\n        (\n            [<PersistentState(StateName.PersonalAccessToken, Grace.Shared.Constants.GraceActorStorage)>] state: IPersistentState<PersonalAccessTokenState>\n        ) =\n        inherit Grain()\n\n        let log = loggerFactory.CreateLogger(\"PersonalAccessToken.Actor\")\n\n        let mutable tokenState = PersonalAccessTokenState.Empty\n\n        override this.OnActivateAsync(ct) =\n            tokenState <- if state.RecordExists then state.State else PersonalAccessTokenState.Empty\n            Task.CompletedTask\n\n        member private this.SaveState() =\n            task {\n                state.State <- tokenState\n\n                if tokenState.Tokens |> List.isEmpty then\n                    do! DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> state.ClearStateAsync())\n                else\n                    do! DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> state.WriteStateAsync())\n            }\n\n        member private _.Summarize(record: PersonalAccessTokenRecord) =\n            {\n                TokenId = record.TokenId\n                Name = record.Name\n                CreatedAt = record.CreatedAt\n                ExpiresAt = record.ExpiresAt\n                LastUsedAt = record.LastUsedAt\n                RevokedAt = record.RevokedAt\n            }\n\n        member private _.ComputeHash (salt: byte array) (secret: byte array) =\n            let combined = Array.zeroCreate<byte> (salt.Length + secret.Length)\n            Array.Copy(salt, 0, combined, 0, salt.Length)\n            Array.Copy(secret, 0, combined, salt.Length, secret.Length)\n            SHA256.HashData combined\n\n        member private this.CreateToken\n            (name: string)\n            (claims: string list)\n            (groupIds: string list)\n            (expiresAt: Instant option)\n            (now: Instant)\n            (correlationId: CorrelationId)\n            =\n            task {\n                let existingName =\n                    tokenState.Tokens\n                    |> List.exists (fun token -> token.Name.Equals(name, StringComparison.OrdinalIgnoreCase))\n\n                if existingName then\n                    return Error(GraceError.Create \"Token name already exists.\" correlationId)\n                else\n                    let tokenId = Guid.NewGuid()\n                    let secret = Array.zeroCreate<byte> 32\n                    let salt = Array.zeroCreate<byte> 32\n                    RandomNumberGenerator.Fill(secret)\n                    RandomNumberGenerator.Fill(salt)\n\n                    let hash = this.ComputeHash salt secret\n\n                    let record =\n                        {\n                            TokenId = tokenId\n                            Name = name\n                            CreatedAt = now\n                            ExpiresAt = expiresAt\n                            LastUsedAt = None\n                            RevokedAt = None\n                            Salt = salt\n                            Hash = hash\n                            Claims = claims\n                            GroupIds = groupIds\n                        }\n\n                    tokenState <- { tokenState with Tokens = record :: tokenState.Tokens }\n                    do! this.SaveState()\n\n                    let userId = this.GetPrimaryKeyString()\n                    let token = formatToken userId tokenId secret\n                    let summary = this.Summarize record\n                    let result = { Token = token; Summary = summary }\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Created PAT for user {UserId}. CorrelationId: {CorrelationId}; TokenId: {TokenId}.\",\n                        getCurrentInstantExtended (),\n                        userId,\n                        correlationId,\n                        tokenId\n                    )\n\n                    return Ok result\n            }\n\n        member private this.ListTokens (includeRevoked: bool) (includeExpired: bool) (now: Instant) (_correlationId: CorrelationId) =\n            task {\n                let isExpired (token: PersonalAccessTokenRecord) =\n                    match token.ExpiresAt with\n                    | None -> false\n                    | Some expiresAt -> expiresAt <= now\n\n                let filtered =\n                    tokenState.Tokens\n                    |> List.filter (fun token ->\n                        let revokedOk = includeRevoked || token.RevokedAt.IsNone\n                        let expiredOk = includeExpired || not (isExpired token)\n                        revokedOk && expiredOk)\n                    |> List.sortByDescending (fun token -> token.CreatedAt)\n                    |> List.map this.Summarize\n\n                return filtered\n            }\n\n        member private this.RevokeToken (tokenId: PersonalAccessTokenId) (now: Instant) (correlationId: CorrelationId) =\n            task {\n                match tokenState.Tokens\n                      |> List.tryFind (fun token -> token.TokenId = tokenId)\n                    with\n                | None -> return Error(GraceError.Create \"Token not found.\" correlationId)\n                | Some record ->\n                    let updated = { record with RevokedAt = Some now }\n\n                    let remaining =\n                        tokenState.Tokens\n                        |> List.filter (fun token -> token.TokenId <> tokenId)\n\n                    tokenState <- { tokenState with Tokens = updated :: remaining }\n                    do! this.SaveState()\n                    return Ok(this.Summarize updated)\n            }\n\n        member private this.ValidateToken (tokenId: PersonalAccessTokenId) (secret: byte array) (now: Instant) (_correlationId: CorrelationId) =\n            task {\n                let isExpired (token: PersonalAccessTokenRecord) =\n                    match token.ExpiresAt with\n                    | None -> false\n                    | Some expiresAt -> expiresAt <= now\n\n                match tokenState.Tokens\n                      |> List.tryFind (fun token -> token.TokenId = tokenId)\n                    with\n                | None -> return None\n                | Some record ->\n                    if record.RevokedAt.IsSome || isExpired record then\n                        return None\n                    else\n                        let computed = this.ComputeHash record.Salt secret\n\n                        if CryptographicOperations.FixedTimeEquals(computed, record.Hash) then\n                            let updated = { record with LastUsedAt = Some now }\n\n                            let remaining =\n                                tokenState.Tokens\n                                |> List.filter (fun token -> token.TokenId <> tokenId)\n\n                            tokenState <- { tokenState with Tokens = updated :: remaining }\n                            do! this.SaveState()\n\n                            let result = { TokenId = record.TokenId; UserId = this.GetPrimaryKeyString(); Claims = record.Claims; GroupIds = record.GroupIds }\n\n                            return Some result\n                        else\n                            return None\n            }\n\n        interface IPersonalAccessTokenActor with\n            member this.CreateToken name claims groupIds expiresAt now correlationId = this.CreateToken name claims groupIds expiresAt now correlationId\n\n            member this.ListTokens includeRevoked includeExpired now correlationId = this.ListTokens includeRevoked includeExpired now correlationId\n\n            member this.RevokeToken tokenId now correlationId = this.RevokeToken tokenId now correlationId\n\n            member this.ValidateToken tokenId secret now correlationId = this.ValidateToken tokenId secret now correlationId\n"
  },
  {
    "path": "src/Grace.Actors/Policy.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Events\nopen Grace.Types.Policy\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule Policy =\n\n    type PolicyActor([<PersistentState(StateName.Policy, Constants.GraceActorStorage)>] state: IPersistentState<List<PolicyEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Policy\n\n        let log = loggerFactory.CreateLogger(\"Policy.Actor\")\n\n        let mutable currentCommand = String.Empty\n\n        let mutable snapshots: PolicySnapshot list = []\n\n        let mutable acknowledgements: PolicyAcknowledgement list = []\n\n        let mutable repositoryId: RepositoryId = RepositoryId.Empty\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            let applyToState (policyEvent: PolicyEvent) =\n                match policyEvent.Event with\n                | SnapshotCreated snapshot ->\n                    snapshots <-\n                        snapshots\n                        |> List.filter (fun s -> s.PolicySnapshotId <> snapshot.PolicySnapshotId)\n                        |> fun list -> list @ [ snapshot ]\n\n                    repositoryId <- snapshot.RepositoryId\n                | Acknowledged (policySnapshotId, acknowledgedBy, note) ->\n                    let acknowledgement =\n                        { PolicySnapshotId = policySnapshotId; AcknowledgedBy = acknowledgedBy; AcknowledgedAt = policyEvent.Metadata.Timestamp; Note = note }\n\n                    acknowledgements <- acknowledgements @ [ acknowledgement ]\n\n            state.State |> Seq.iter applyToState\n\n            Task.CompletedTask\n\n        member private this.GetCurrentSnapshot() =\n            snapshots\n            |> List.sortBy (fun snapshot -> snapshot.CreatedAt)\n            |> List.tryLast\n\n        member private this.ApplyEvent(policyEvent: PolicyEvent) =\n            task {\n                let correlationId = policyEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(policyEvent)\n                    do! state.WriteStateAsync()\n\n                    match policyEvent.Event with\n                    | SnapshotCreated snapshot ->\n                        snapshots <-\n                            snapshots\n                            |> List.filter (fun s -> s.PolicySnapshotId <> snapshot.PolicySnapshotId)\n                            |> fun list -> list @ [ snapshot ]\n\n                        repositoryId <- snapshot.RepositoryId\n                    | Acknowledged (policySnapshotId, acknowledgedBy, note) ->\n                        let acknowledgement =\n                            {\n                                PolicySnapshotId = policySnapshotId\n                                AcknowledgedBy = acknowledgedBy\n                                AcknowledgedAt = policyEvent.Metadata.Timestamp\n                                Note = note\n                            }\n\n                        acknowledgements <- acknowledgements @ [ acknowledgement ]\n\n                    let graceEvent = GraceEvent.PolicyEvent policyEvent\n                    do! publishGraceEvent graceEvent policyEvent.Metadata\n\n                    let policySnapshotId =\n                        match policyEvent.Event with\n                        | SnapshotCreated snapshot -> snapshot.PolicySnapshotId\n                        | Acknowledged (policySnapshotId, _, _) -> policySnapshotId\n\n                    let returnValue =\n                        (GraceReturnValue.Create \"Policy command succeeded.\" correlationId)\n                            .enhance(nameof RepositoryId, repositoryId)\n                            .enhance(nameof PolicySnapshotId, policySnapshotId)\n                            .enhance (nameof PolicyEventType, getDiscriminatedUnionFullName policyEvent.Event)\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Failed to apply event {eventType} for policy.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        getDiscriminatedUnionCaseName policyEvent.Event\n                    )\n\n                    let graceError =\n                        (GraceError.CreateWithException ex (PolicyError.getErrorMessage PolicyError.FailedWhileApplyingEvent) correlationId)\n                            .enhance (nameof RepositoryId, repositoryId)\n\n                    return Error graceError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = repositoryId |> returnTask\n\n        interface IPolicyActor with\n            member this.GetCurrent correlationId =\n                this.correlationId <- correlationId\n                this.GetCurrentSnapshot() |> returnTask\n\n            member this.GetSnapshots correlationId =\n                this.correlationId <- correlationId\n\n                (snapshots :> IReadOnlyList<PolicySnapshot>)\n                |> returnTask\n\n            member this.GetAcknowledgements correlationId =\n                this.correlationId <- correlationId\n\n                (acknowledgements :> IReadOnlyList<PolicyAcknowledgement>)\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (command: PolicyCommand) (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (PolicyError.getErrorMessage PolicyError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | CreateSnapshot snapshot ->\n                                let exists =\n                                    snapshots\n                                    |> List.exists (fun existing -> existing.PolicySnapshotId = snapshot.PolicySnapshotId)\n\n                                if exists then\n                                    return Error(GraceError.Create (PolicyError.getErrorMessage PolicyError.PolicySnapshotAlreadyExists) metadata.CorrelationId)\n                                else\n                                    return Ok command\n                            | Acknowledge (policySnapshotId, _, _) ->\n                                let exists =\n                                    snapshots\n                                    |> List.exists (fun existing -> existing.PolicySnapshotId = policySnapshotId)\n\n                                if exists then\n                                    return Ok command\n                                else\n                                    return Error(GraceError.Create (PolicyError.getErrorMessage PolicyError.PolicySnapshotDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let processCommand (command: PolicyCommand) (metadata: EventMetadata) =\n                    task {\n                        let! policyEventType =\n                            task {\n                                match command with\n                                | CreateSnapshot snapshot -> return SnapshotCreated snapshot\n                                | Acknowledge (policySnapshotId, acknowledgedBy, note) -> return Acknowledged(policySnapshotId, acknowledgedBy, note)\n                            }\n\n                        let policyEvent = { Event = policyEventType; Metadata = metadata }\n                        return! this.ApplyEvent policyEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/PromotionQueue.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Events\nopen Grace.Types.Queue\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule PromotionQueue =\n\n    type PromotionQueueActor([<PersistentState(StateName.PromotionQueue, Constants.GraceActorStorage)>] state: IPersistentState<List<PromotionQueueEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.PromotionQueue\n\n        let log = loggerFactory.CreateLogger(\"PromotionQueue.Actor\")\n\n        let mutable currentCommand = String.Empty\n\n        let mutable promotionQueue = PromotionQueue.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            promotionQueue <-\n                state.State\n                |> Seq.fold (fun dto ev -> PromotionQueueDto.UpdateDto ev dto) promotionQueue\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent(queueEvent: PromotionQueueEvent) =\n            task {\n                let correlationId = queueEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(queueEvent)\n                    do! state.WriteStateAsync()\n\n                    promotionQueue <-\n                        promotionQueue\n                        |> PromotionQueueDto.UpdateDto queueEvent\n\n                    let graceEvent = GraceEvent.QueueEvent queueEvent\n                    do! publishGraceEvent graceEvent queueEvent.Metadata\n\n                    let returnValue =\n                        (GraceReturnValue.Create \"Promotion queue command succeeded.\" correlationId)\n                            .enhance(nameof BranchId, promotionQueue.TargetBranchId)\n                            .enhance (nameof PromotionQueueEventType, getDiscriminatedUnionFullName queueEvent.Event)\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Failed to apply event {eventType} for promotion queue {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        getDiscriminatedUnionCaseName queueEvent.Event,\n                        promotionQueue.TargetBranchId\n                    )\n\n                    let graceError =\n                        (GraceError.CreateWithException ex (QueueError.getErrorMessage QueueError.FailedWhileApplyingEvent) correlationId)\n                            .enhance (nameof BranchId, promotionQueue.TargetBranchId)\n\n                    return Error graceError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = RepositoryId.Empty |> returnTask\n\n        interface IPromotionQueueActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                (promotionQueue.TargetBranchId <> BranchId.Empty)\n                |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                promotionQueue |> returnTask\n\n            member this.GetEvents correlationId =\n                this.correlationId <- correlationId\n\n                state.State :> IReadOnlyList<PromotionQueueEvent>\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (command: PromotionQueueCommand) (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (QueueError.getErrorMessage QueueError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            let result =\n                                match command with\n                                | Initialize _ ->\n                                    if promotionQueue.TargetBranchId <> BranchId.Empty then\n                                        Error(GraceError.Create (QueueError.getErrorMessage QueueError.QueueAlreadyInitialized) metadata.CorrelationId)\n                                    else\n                                        Ok command\n                                | _ ->\n                                    if promotionQueue.TargetBranchId = BranchId.Empty then\n                                        Error(GraceError.Create (QueueError.getErrorMessage QueueError.QueueNotInitialized) metadata.CorrelationId)\n                                    else\n                                        match command with\n                                        | Dequeue promotionSetId ->\n                                            let exists =\n                                                promotionQueue.PromotionSetIds\n                                                |> List.exists (fun existing -> existing = promotionSetId)\n\n                                            if exists then\n                                                Ok command\n                                            else\n                                                Error(GraceError.Create (QueueError.getErrorMessage QueueError.PromotionSetNotInQueue) metadata.CorrelationId)\n                                        | SetRunning (Some promotionSetId) ->\n                                            let exists =\n                                                promotionQueue.PromotionSetIds\n                                                |> List.exists (fun existing -> existing = promotionSetId)\n\n                                            if exists then\n                                                Ok command\n                                            else\n                                                Error(GraceError.Create (QueueError.getErrorMessage QueueError.PromotionSetNotInQueue) metadata.CorrelationId)\n                                        | _ -> Ok command\n\n                            return result\n                    }\n\n                let processCommand (command: PromotionQueueCommand) (metadata: EventMetadata) =\n                    task {\n                        let! (queueEventType: PromotionQueueEventType) =\n                            task {\n                                match command with\n                                | Initialize (targetBranchId, policySnapshotId) -> return Initialized(targetBranchId, policySnapshotId)\n                                | Enqueue promotionSetId -> return PromotionSetEnqueued promotionSetId\n                                | Dequeue promotionSetId -> return PromotionSetDequeued promotionSetId\n                                | SetRunning promotionSetId -> return RunningPromotionSetSet promotionSetId\n                                | Pause -> return Paused\n                                | Resume -> return Resumed\n                                | SetDegraded -> return Degraded\n                                | UpdatePolicySnapshot policySnapshotId -> return PolicySnapshotUpdated policySnapshotId\n                            }\n\n                        let queueEvent: PromotionQueueEvent = { Event = queueEventType; Metadata = metadata }\n                        return! this.ApplyEvent queueEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/PromotionSet.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Specialized\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Services\nopen Grace.Shared.Utilities\nopen Grace.Types.Artifact\nopen Grace.Types.Events\nopen Grace.Types.PromotionSetConflictModel\nopen Grace.Types.PromotionSet\nopen Grace.Types.Queue\nopen Grace.Types.Reference\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Globalization\nopen System.IO\nopen System.IO.Compression\nopen System.Text\nopen System.Threading.Tasks\n\nmodule PromotionSet =\n\n    type private RecomputeFailure =\n        | Blocked of reason: string * artifactId: ArtifactId option\n        | Failed of reason: string\n\n    type private DirectorySnapshot = { DirectoriesByPath: Dictionary<RelativePath, DirectoryVersion>; FilesByPath: Dictionary<RelativePath, FileVersion> }\n\n    type private StepConflictFile =\n        {\n            FilePath: RelativePath\n            BaseFile: FileVersion option\n            OursFile: FileVersion option\n            TheirsFile: FileVersion option\n            IsBinary: bool\n        }\n\n    let internal validateCommandForState\n        (existingEvents: seq<PromotionSetEvent>)\n        (currentPromotionSetDto: PromotionSetDto)\n        (promotionSetCommand: PromotionSetCommand)\n        (eventMetadata: EventMetadata)\n        =\n        if existingEvents\n           |> Seq.exists (fun event -> event.Metadata.CorrelationId = eventMetadata.CorrelationId) then\n            Error(GraceError.Create \"Duplicate correlation ID for PromotionSet command.\" eventMetadata.CorrelationId)\n        else\n            match promotionSetCommand with\n            | PromotionSetCommand.CreatePromotionSet _ when\n                currentPromotionSetDto.PromotionSetId\n                <> PromotionSetId.Empty\n                ->\n                Error(GraceError.Create \"PromotionSet already exists.\" eventMetadata.CorrelationId)\n            | PromotionSetCommand.CreatePromotionSet _ -> Ok promotionSetCommand\n            | _ when currentPromotionSetDto.PromotionSetId = PromotionSetId.Empty ->\n                Error(GraceError.Create \"PromotionSet does not exist.\" eventMetadata.CorrelationId)\n            | PromotionSetCommand.Apply when currentPromotionSetDto.Status = PromotionSetStatus.Succeeded ->\n                Error(GraceError.Create \"PromotionSet has already been applied successfully.\" eventMetadata.CorrelationId)\n            | PromotionSetCommand.Apply when currentPromotionSetDto.Status = PromotionSetStatus.Running ->\n                Error(GraceError.Create \"PromotionSet is already running.\" eventMetadata.CorrelationId)\n            | PromotionSetCommand.RecomputeStepsIfStale _ when currentPromotionSetDto.StepsComputationStatus = StepsComputationStatus.Computing ->\n                Error(GraceError.Create \"PromotionSet steps are already computing.\" eventMetadata.CorrelationId)\n            | PromotionSetCommand.ResolveConflicts _ when\n                currentPromotionSetDto.Status\n                <> PromotionSetStatus.Blocked\n                ->\n                Error(GraceError.Create \"PromotionSet is not blocked for conflict review.\" eventMetadata.CorrelationId)\n            | PromotionSetCommand.UpdateInputPromotions _ when currentPromotionSetDto.Status = PromotionSetStatus.Succeeded ->\n                Error(GraceError.Create \"PromotionSet has already succeeded and cannot be edited.\" eventMetadata.CorrelationId)\n            | _ -> Ok promotionSetCommand\n\n    type PromotionSetActor([<PersistentState(StateName.PromotionSet, Constants.GraceActorStorage)>] state: IPersistentState<List<PromotionSetEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.PromotionSet\n        let log = loggerFactory.CreateLogger(\"PromotionSet.Actor\")\n\n        let mutable currentCommand = String.Empty\n        let mutable promotionSetDto = PromotionSetDto.Default\n\n        let getIntEnvironmentSetting (name: string) (defaultValue: int) =\n            let rawValue = Environment.GetEnvironmentVariable(name)\n            let mutable parsedValue = 0\n\n            if String.IsNullOrWhiteSpace rawValue |> not\n               && Int32.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, &parsedValue)\n               && parsedValue > 0 then\n                parsedValue\n            else\n                defaultValue\n\n        let maxStepsPerRecompute = getIntEnvironmentSetting \"grace__promotionset__recompute__max_steps\" 1000\n\n        let maxStepTimeMilliseconds = getIntEnvironmentSetting \"grace__promotionset__recompute__max_step_time_ms\" 30000\n\n        let maxTotalTimeMilliseconds = getIntEnvironmentSetting \"grace__promotionset__recompute__max_total_time_ms\" 300000\n\n        let maxFilesPerStep = getIntEnvironmentSetting \"grace__promotionset__recompute__max_files_per_step\" 20000\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        member private this.WithActorMetadata(metadata: EventMetadata) =\n            let properties = Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n\n            metadata.Properties\n            |> Seq.iter (fun kvp -> properties[kvp.Key] <- kvp.Value)\n\n            if promotionSetDto.RepositoryId <> RepositoryId.Empty then\n                properties[nameof RepositoryId] <- $\"{promotionSetDto.RepositoryId}\"\n\n            let actorId =\n                if promotionSetDto.PromotionSetId\n                   <> PromotionSetId.Empty then\n                    $\"{promotionSetDto.PromotionSetId}\"\n                else\n                    $\"{this.GetPrimaryKey()}\"\n\n            properties[\"ActorId\"] <- actorId\n\n            let principal =\n                if String.IsNullOrWhiteSpace metadata.Principal then\n                    GraceSystemUser\n                else\n                    metadata.Principal\n\n            { metadata with Principal = principal; Properties = properties }\n\n        member private this.BuildSuccess(message: string, correlationId: CorrelationId) =\n            let graceReturnValue: GraceReturnValue<string> =\n                (GraceReturnValue.Create message correlationId)\n                    .enhance(nameof RepositoryId, promotionSetDto.RepositoryId)\n                    .enhance(nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                    .enhance (\"Status\", getDiscriminatedUnionCaseName promotionSetDto.Status)\n\n            Ok graceReturnValue\n\n        member private this.BuildError(errorMessage: string, correlationId: CorrelationId) =\n            Error(\n                (GraceError.Create errorMessage correlationId)\n                    .enhance(nameof RepositoryId, promotionSetDto.RepositoryId)\n                    .enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n            )\n\n        member private this.GetCurrentTerminalPromotion() =\n            task {\n                let! latestPromotion = getLatestPromotion promotionSetDto.RepositoryId promotionSetDto.TargetBranchId\n\n                match latestPromotion with\n                | Option.Some promotion -> return promotion.ReferenceId, promotion.DirectoryId\n                | Option.None ->\n                    let branchActorProxy = Branch.CreateActorProxy promotionSetDto.TargetBranchId promotionSetDto.RepositoryId this.correlationId\n                    let! branchDto = branchActorProxy.Get this.correlationId\n\n                    let baseDirectoryVersionId =\n                        if branchDto.BasedOn.ReferenceId <> ReferenceId.Empty then\n                            branchDto.BasedOn.DirectoryId\n                        elif branchDto.LatestPromotion.ReferenceId\n                             <> ReferenceId.Empty then\n                            branchDto.LatestPromotion.DirectoryId\n                        elif branchDto.LatestReference.ReferenceId\n                             <> ReferenceId.Empty then\n                            branchDto.LatestReference.DirectoryId\n                        else\n                            DirectoryVersionId.Empty\n\n                    return ReferenceId.Empty, baseDirectoryVersionId\n            }\n\n        member private this.GetConflictResolutionPolicy() =\n            task {\n                let repositoryActorProxy = Repository.CreateActorProxy promotionSetDto.OrganizationId promotionSetDto.RepositoryId this.correlationId\n                let! repositoryDto = repositoryActorProxy.Get this.correlationId\n                return repositoryDto.ConflictResolutionPolicy\n            }\n\n        member private this.GetRepositoryDto() =\n            task {\n                let repositoryActorProxy = Repository.CreateActorProxy promotionSetDto.OrganizationId promotionSetDto.RepositoryId this.correlationId\n                return! repositoryActorProxy.Get this.correlationId\n            }\n\n        member private this.GetConflictResolutionModelProvider() =\n            match hostServiceProvider with\n            | null -> NullConflictResolutionModelProvider() :> IConflictResolutionModelProvider\n            | services ->\n                match services.GetService(typeof<IConflictResolutionModelProvider>) with\n                | null -> NullConflictResolutionModelProvider() :> IConflictResolutionModelProvider\n                | provider -> provider :?> IConflictResolutionModelProvider\n\n        member private this.UploadArtifactPayload(blobPath: string, payloadJson: string, metadata: EventMetadata) =\n            task {\n                try\n                    let! repositoryDto = this.GetRepositoryDto()\n                    let! uploadUri = getUriWithWriteSharedAccessSignature repositoryDto blobPath metadata.CorrelationId\n                    let blockBlobClient = BlockBlobClient(uploadUri)\n                    let payloadBytes = Encoding.UTF8.GetBytes(payloadJson)\n                    use payloadStream = new MemoryStream(payloadBytes)\n                    do! blockBlobClient.UploadAsync(payloadStream) :> Task\n                    return Ok()\n                with\n                | ex ->\n                    let graceError = GraceError.CreateWithException ex \"Failed while uploading Artifact payload.\" metadata.CorrelationId\n\n                    graceError.enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                    |> ignore\n\n                    graceError.enhance (nameof RepositoryId, promotionSetDto.RepositoryId)\n                    |> ignore\n\n                    return Error(graceError)\n            }\n\n        member private this.GetArtifactText(artifactId: ArtifactId, metadata: EventMetadata) =\n            task {\n                let artifactActorProxy = Artifact.CreateActorProxy artifactId promotionSetDto.RepositoryId this.correlationId\n                let! artifact = artifactActorProxy.Get this.correlationId\n\n                match artifact with\n                | Option.None ->\n                    let graceError = GraceError.Create \"Manual override artifact was not found.\" metadata.CorrelationId\n\n                    graceError.enhance (nameof ArtifactId, artifactId)\n                    |> ignore\n\n                    graceError.enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                    |> ignore\n\n                    return Error(graceError)\n                | Option.Some artifactMetadata ->\n                    try\n                        let! repositoryDto = this.GetRepositoryDto()\n                        let! downloadUri = getUriWithReadSharedAccessSignature repositoryDto artifactMetadata.BlobPath metadata.CorrelationId\n                        let blobClient = BlobClient(downloadUri)\n                        let! downloadResult = blobClient.DownloadContentAsync()\n                        return Ok(downloadResult.Value.Content.ToString())\n                    with\n                    | ex ->\n                        let graceError = GraceError.CreateWithException ex \"Failed while downloading manual override artifact payload.\" metadata.CorrelationId\n\n                        graceError.enhance (nameof ArtifactId, artifactId)\n                        |> ignore\n\n                        graceError.enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                        |> ignore\n\n                        return Error(graceError)\n            }\n\n        member private this.ValidateManualOverrideArtifacts(decisions: ConflictResolutionDecision list, metadata: EventMetadata) =\n            task {\n                let overrideArtifactIds =\n                    decisions\n                    |> List.choose (fun decision -> if decision.Accepted then decision.OverrideContentArtifactId else Option.None)\n                    |> List.distinct\n\n                let mutable validationError: GraceError option = Option.None\n                let mutable index = 0\n\n                while index < overrideArtifactIds.Length\n                      && validationError.IsNone do\n                    let artifactId = overrideArtifactIds[index]\n                    let! artifactTextResult = this.GetArtifactText(artifactId, metadata)\n\n                    match artifactTextResult with\n                    | Error graceError -> validationError <- Some graceError\n                    | Ok artifactText ->\n                        if String.IsNullOrWhiteSpace artifactText then\n                            let graceError = GraceError.Create \"Manual override artifact content is empty.\" metadata.CorrelationId\n\n                            graceError.enhance (nameof ArtifactId, artifactId)\n                            |> ignore\n\n                            graceError.enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                            |> ignore\n\n                            validationError <- Some graceError\n\n                    index <- index + 1\n\n                match validationError with\n                | Some graceError -> return Error graceError\n                | Option.None -> return Ok()\n            }\n\n        member private this.HydrateStepProvenance(step: PromotionSetStep) =\n            task {\n                let referenceActorProxy = Reference.CreateActorProxy step.OriginalPromotion.ReferenceId promotionSetDto.RepositoryId this.correlationId\n                let! promotionReferenceDto = referenceActorProxy.Get this.correlationId\n\n                let promotionDirectoryVersionId =\n                    if promotionReferenceDto.ReferenceId\n                       <> ReferenceId.Empty then\n                        promotionReferenceDto.DirectoryId\n                    else\n                        step.OriginalPromotion.DirectoryVersionId\n\n                if promotionDirectoryVersionId = DirectoryVersionId.Empty then\n                    return\n                        Error(\n                            (GraceError.Create \"Original promotion did not include a directory version.\" this.correlationId)\n                                .enhance (nameof PromotionSetStepId, step.StepId)\n                        )\n                else\n                    let basedOnReferenceId =\n                        if step.OriginalBasePromotionReferenceId\n                           <> ReferenceId.Empty then\n                            step.OriginalBasePromotionReferenceId\n                        elif promotionReferenceDto.ReferenceId\n                             <> ReferenceId.Empty then\n                            promotionReferenceDto.Links\n                            |> Seq.tryPick (fun link ->\n                                match link with\n                                | ReferenceLinkType.BasedOn referenceId -> Option.Some referenceId\n                                | _ -> Option.None)\n                            |> Option.defaultValue ReferenceId.Empty\n                        else\n                            ReferenceId.Empty\n\n                    let! basedOnDirectoryVersionId =\n                        if step.OriginalBaseDirectoryVersionId\n                           <> DirectoryVersionId.Empty then\n                            Task.FromResult step.OriginalBaseDirectoryVersionId\n                        elif basedOnReferenceId <> ReferenceId.Empty then\n                            task {\n                                let basedOnReferenceActorProxy = Reference.CreateActorProxy basedOnReferenceId promotionSetDto.RepositoryId this.correlationId\n\n                                let! basedOnReferenceDto = basedOnReferenceActorProxy.Get this.correlationId\n\n                                if basedOnReferenceDto.ReferenceId = ReferenceId.Empty then\n                                    return promotionDirectoryVersionId\n                                else\n                                    return basedOnReferenceDto.DirectoryId\n                            }\n                        else\n                            Task.FromResult promotionDirectoryVersionId\n\n                    return\n                        Ok\n                            { step with\n                                OriginalPromotion = { step.OriginalPromotion with DirectoryVersionId = promotionDirectoryVersionId }\n                                OriginalBasePromotionReferenceId = basedOnReferenceId\n                                OriginalBaseDirectoryVersionId = basedOnDirectoryVersionId\n                            }\n            }\n\n        member private this.TryGetFileVersion(fileLookup: Dictionary<RelativePath, FileVersion>, filePath: RelativePath) =\n            let mutable fileVersion = FileVersion.Default\n\n            if fileLookup.TryGetValue(filePath, &fileVersion) then\n                Option.Some fileVersion\n            else\n                Option.None\n\n        member private this.FileVersionEquivalent(left: FileVersion option, right: FileVersion option) =\n            match left, right with\n            | Option.None, Option.None -> true\n            | Option.Some leftFile, Option.Some rightFile -> leftFile.Sha256Hash = rightFile.Sha256Hash\n            | _ -> false\n\n        member private this.LoadDirectorySnapshot(directoryVersionId: DirectoryVersionId) =\n            task {\n                let directoriesByPath = Dictionary<RelativePath, DirectoryVersion>(StringComparer.OrdinalIgnoreCase)\n                let filesByPath = Dictionary<RelativePath, FileVersion>(StringComparer.OrdinalIgnoreCase)\n\n                if directoryVersionId = DirectoryVersionId.Empty then\n                    return Ok { DirectoriesByPath = directoriesByPath; FilesByPath = filesByPath }\n                else\n                    let directoryVersionActorProxy = DirectoryVersion.CreateActorProxy directoryVersionId promotionSetDto.RepositoryId this.correlationId\n                    let! rootDirectoryVersion = directoryVersionActorProxy.Get this.correlationId\n\n                    if rootDirectoryVersion.DirectoryVersion.DirectoryVersionId = DirectoryVersionId.Empty then\n                        let graceError = GraceError.Create \"DirectoryVersion was not found while recomputing PromotionSet step.\" this.correlationId\n\n                        graceError.enhance (nameof DirectoryVersionId, directoryVersionId)\n                        |> ignore\n\n                        graceError.enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                        |> ignore\n\n                        return Error graceError\n                    else\n                        let! recursiveDirectoryVersions = directoryVersionActorProxy.GetRecursiveDirectoryVersions false this.correlationId\n                        let mutable directoryIndex = 0\n\n                        while directoryIndex < recursiveDirectoryVersions.Length do\n                            let directoryVersion =\n                                recursiveDirectoryVersions[directoryIndex]\n                                    .DirectoryVersion\n\n                            directoriesByPath[directoryVersion.RelativePath] <- directoryVersion\n                            let filesInDirectory = directoryVersion.Files\n                            let mutable fileIndex = 0\n\n                            while fileIndex < filesInDirectory.Count do\n                                let fileVersion = filesInDirectory[fileIndex]\n                                filesByPath[fileVersion.RelativePath] <- fileVersion\n                                fileIndex <- fileIndex + 1\n\n                            directoryIndex <- directoryIndex + 1\n\n                        return Ok { DirectoriesByPath = directoriesByPath; FilesByPath = filesByPath }\n            }\n\n        member private this.ReadTextFileVersion(repositoryDto, fileVersion: FileVersion option, correlationId: CorrelationId) =\n            task {\n                match fileVersion with\n                | Option.None -> return Ok Option.None\n                | Option.Some currentFileVersion when currentFileVersion.IsBinary -> return Ok Option.None\n                | Option.Some currentFileVersion ->\n                    try\n                        let! readUri = getUriWithReadSharedAccessSignatureForFileVersion repositoryDto currentFileVersion correlationId\n                        let blobClient = BlockBlobClient(readUri)\n                        use! blobStream = blobClient.OpenReadAsync(position = 0, bufferSize = (64 * 1024))\n\n                        use contentStream = new GZipStream(stream = blobStream, mode = CompressionMode.Decompress, leaveOpen = false) :> Stream\n\n                        use streamReader = new StreamReader(contentStream, Encoding.UTF8, true, 16 * 1024, leaveOpen = false)\n                        let! textContents = streamReader.ReadToEndAsync()\n                        return Ok(Option.Some textContents)\n                    with\n                    | ex ->\n                        return\n                            Error(\n                                (GraceError.CreateWithException ex \"Failed while reading conflicted text file for PromotionSet recompute.\" correlationId)\n                                    .enhance(nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                                    .enhance (\"FilePath\", currentFileVersion.RelativePath)\n                            )\n            }\n\n        member private this.CreateTextOverrideFileVersion(filePath: RelativePath, textContent: string, repositoryDto, metadata: EventMetadata) =\n            task {\n                try\n                    let payloadBytes = Encoding.UTF8.GetBytes(textContent)\n                    use hashStream = new MemoryStream(payloadBytes)\n                    let! sha256Hash = computeSha256ForFile hashStream filePath\n\n                    let fileVersion = FileVersion.Create filePath sha256Hash String.Empty false (int64 payloadBytes.Length)\n\n                    let! writeUri = getUriWithWriteSharedAccessSignatureForFileVersion repositoryDto fileVersion metadata.CorrelationId\n                    let blobClient = BlobClient(writeUri)\n                    use uploadStream = new MemoryStream()\n\n                    use gzipStream = new GZipStream(uploadStream, CompressionLevel.Optimal, leaveOpen = true)\n                    gzipStream.Write(payloadBytes, 0, payloadBytes.Length)\n                    gzipStream.Flush()\n                    gzipStream.Dispose()\n\n                    uploadStream.Position <- 0L\n                    do! blobClient.UploadAsync(uploadStream, overwrite = true) :> Task\n                    return Ok fileVersion\n                with\n                | ex ->\n                    return\n                        Error(\n                            (GraceError.CreateWithException ex \"Failed while creating manual override file version.\" metadata.CorrelationId)\n                                .enhance(nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                                .enhance (\"FilePath\", filePath)\n                        )\n            }\n\n        member private this.ReadConflictTextPair(repositoryDto, conflictFile: StepConflictFile) =\n            task {\n                let! oursTextResult = this.ReadTextFileVersion(repositoryDto, conflictFile.OursFile, this.correlationId)\n\n                match oursTextResult with\n                | Error graceError -> return Error graceError\n                | Ok oursTextOption ->\n                    let! theirsTextResult = this.ReadTextFileVersion(repositoryDto, conflictFile.TheirsFile, this.correlationId)\n\n                    match theirsTextResult with\n                    | Error graceError -> return Error graceError\n                    | Ok theirsTextOption ->\n                        let oursContent =\n                            match oursTextOption with\n                            | Option.Some text -> text\n                            | Option.None ->\n                                match conflictFile.OursFile with\n                                | Option.Some fileVersion when fileVersion.IsBinary -> $\"<binary sha={fileVersion.Sha256Hash}>\"\n                                | Option.Some _ -> \"<text unavailable>\"\n                                | Option.None -> \"<deleted>\"\n\n                        let theirsContent =\n                            match theirsTextOption with\n                            | Option.Some text -> text\n                            | Option.None ->\n                                match conflictFile.TheirsFile with\n                                | Option.Some fileVersion when fileVersion.IsBinary -> $\"<binary sha={fileVersion.Sha256Hash}>\"\n                                | Option.Some _ -> \"<text unavailable>\"\n                                | Option.None -> \"<deleted>\"\n\n                        return Ok(oursContent, theirsContent)\n            }\n\n        member private this.ResolveConflictWithModel(repositoryDto, conflictFile: StepConflictFile, metadata: EventMetadata) =\n            task {\n                let modelProvider = this.GetConflictResolutionModelProvider()\n                let! baseTextResult = this.ReadTextFileVersion(repositoryDto, conflictFile.BaseFile, metadata.CorrelationId)\n\n                match baseTextResult with\n                | Error graceError -> return Error graceError.Error\n                | Ok baseTextOption ->\n                    let! oursTextResult = this.ReadTextFileVersion(repositoryDto, conflictFile.OursFile, metadata.CorrelationId)\n\n                    match oursTextResult with\n                    | Error graceError -> return Error graceError.Error\n                    | Ok oursTextOption ->\n                        let! theirsTextResult = this.ReadTextFileVersion(repositoryDto, conflictFile.TheirsFile, metadata.CorrelationId)\n\n                        match theirsTextResult with\n                        | Error graceError -> return Error graceError.Error\n                        | Ok theirsTextOption ->\n                            let request: ConflictResolutionModelRequest =\n                                {\n                                    FilePath = conflictFile.FilePath\n                                    BaseContent = baseTextOption\n                                    OursContent = oursTextOption\n                                    TheirsContent = theirsTextOption\n                                }\n\n                            let! modelResponseResult = modelProvider.SuggestResolution request\n\n                            match modelResponseResult with\n                            | Error errorText ->\n                                return\n                                    Error(\n                                        sprintf\n                                            \"Model resolution failed for file '%s' using provider '%s': %s\"\n                                            conflictFile.FilePath\n                                            modelProvider.ProviderName\n                                            errorText\n                                    )\n                            | Ok modelResponse ->\n                                let modelResolutionSummary =\n                                    match modelResponse.Explanation with\n                                    | Option.Some explanation when not <| String.IsNullOrWhiteSpace explanation -> explanation\n                                    | _ ->\n                                        if modelResponse.ShouldDelete then\n                                            \"Model proposed deleting the conflicted file.\"\n                                        else\n                                            \"Model-suggested merge proposal.\"\n\n                                if modelResponse.ShouldDelete then\n                                    return\n                                        Ok(\n                                            { ModelResolution = modelResolutionSummary; Confidence = modelResponse.Confidence; Accepted = Option.None },\n                                            Option.None\n                                        )\n                                else\n                                    match modelResponse.ProposedContent with\n                                    | Option.None ->\n                                        return Error(sprintf \"Model response for file '%s' did not include proposed content.\" conflictFile.FilePath)\n                                    | Some proposedContent ->\n                                        let! overrideFileVersionResult =\n                                            this.CreateTextOverrideFileVersion(conflictFile.FilePath, proposedContent, repositoryDto, metadata)\n\n                                        match overrideFileVersionResult with\n                                        | Error graceError -> return Error graceError.Error\n                                        | Ok overrideFileVersion ->\n                                            return\n                                                Ok(\n                                                    { ModelResolution = modelResolutionSummary; Confidence = modelResponse.Confidence; Accepted = Option.None },\n                                                    Option.Some overrideFileVersion\n                                                )\n            }\n\n        member private this.BuildConflictAnalyses\n            (\n                repositoryDto,\n                conflictFiles: StepConflictFile list,\n                outcomesByPath: Dictionary<RelativePath, ConflictResolutionOutcome>,\n                resolutionMethodByPath: Dictionary<RelativePath, ConflictResolutionMethod>\n            ) =\n            task {\n                let! conflictTextResults =\n                    conflictFiles\n                    |> List.map (fun conflictFile ->\n                        task {\n                            let! conflictTextResult = this.ReadConflictTextPair(repositoryDto, conflictFile)\n                            return conflictFile, conflictTextResult\n                        })\n                    |> Task.WhenAll\n\n                let firstError =\n                    conflictTextResults\n                    |> Seq.tryPick (fun (_, result) ->\n                        match result with\n                        | Error graceError -> Option.Some graceError\n                        | Ok _ -> Option.None)\n\n                match firstError with\n                | Option.Some graceError -> return Error graceError\n                | Option.None ->\n                    let conflictAnalyses = ResizeArray<ConflictAnalysis>()\n\n                    conflictTextResults\n                    |> Seq.iter (fun (conflictFile, result) ->\n                        match result with\n                        | Error _ -> ()\n                        | Ok (oursContent, theirsContent) ->\n                            let lineCount =\n                                max\n                                    1\n                                    (max\n                                        (if String.IsNullOrEmpty oursContent then 0 else oursContent.Split('\\n').Length)\n                                        (if String.IsNullOrEmpty theirsContent then\n                                             0\n                                         else\n                                             theirsContent.Split('\\n').Length))\n\n                            let mutable resolvedOutcome = Unchecked.defaultof<ConflictResolutionOutcome>\n\n                            let proposedResolution =\n                                if outcomesByPath.TryGetValue(conflictFile.FilePath, &resolvedOutcome) then\n                                    Option.Some resolvedOutcome\n                                else\n                                    Option.None\n\n                            let mutable resolutionMethod = Unchecked.defaultof<ConflictResolutionMethod>\n\n                            let resolvedMethod =\n                                if resolutionMethodByPath.TryGetValue(conflictFile.FilePath, &resolutionMethod) then\n                                    resolutionMethod\n                                else\n                                    ConflictResolutionMethod.None\n\n                            conflictAnalyses.Add(\n                                {\n                                    FilePath = conflictFile.FilePath\n                                    OriginalHunks =\n                                        [\n                                            { StartLine = 1; EndLine = lineCount; OursContent = oursContent; TheirsContent = theirsContent }\n                                        ]\n                                    ProposedResolution = proposedResolution\n                                    ResolutionMethod = resolvedMethod\n                                }\n                            ))\n\n                    return Ok(conflictAnalyses |> Seq.toList)\n            }\n\n        member private this.MaterializeMergedDirectoryVersion\n            (\n                repositoryDto,\n                baseSnapshot: DirectorySnapshot,\n                oursSnapshot: DirectorySnapshot,\n                theirsSnapshot: DirectorySnapshot,\n                mergedFilesByPath: Dictionary<RelativePath, FileVersion>,\n                metadata: EventMetadata\n            ) =\n            task {\n                let directoryPaths = HashSet<RelativePath>(StringComparer.OrdinalIgnoreCase)\n                directoryPaths.Add(RootDirectoryPath) |> ignore\n                let filesByDirectory = Dictionary<RelativePath, ResizeArray<FileVersion>>(StringComparer.OrdinalIgnoreCase)\n                let mergedFileValues = mergedFilesByPath.Values |> Seq.toArray\n                let mutable fileIndex = 0\n\n                while fileIndex < mergedFileValues.Length do\n                    let fileVersion = mergedFileValues[fileIndex]\n                    let fileDirectoryPath = getRelativeDirectory fileVersion.RelativePath RootDirectoryPath\n                    let mutable currentDirectoryPath = fileDirectoryPath\n                    let mutable continueUpTree = true\n\n                    while continueUpTree do\n                        directoryPaths.Add(currentDirectoryPath) |> ignore\n\n                        match getParentPath currentDirectoryPath with\n                        | Option.Some parentPath -> currentDirectoryPath <- parentPath\n                        | Option.None -> continueUpTree <- false\n\n                    let mutable filesInDirectory = Unchecked.defaultof<ResizeArray<FileVersion>>\n\n                    if\n                        not\n                        <| filesByDirectory.TryGetValue(fileDirectoryPath, &filesInDirectory)\n                    then\n                        filesInDirectory <- ResizeArray<FileVersion>()\n                        filesByDirectory[fileDirectoryPath] <- filesInDirectory\n\n                    filesInDirectory.Add(fileVersion)\n                    fileIndex <- fileIndex + 1\n\n                let orderedDirectoryPaths =\n                    directoryPaths\n                    |> Seq.sortByDescending countSegments\n                    |> Seq.toArray\n\n                let childDirectoriesByParent = Dictionary<RelativePath, ResizeArray<RelativePath>>(StringComparer.OrdinalIgnoreCase)\n                let mutable directoryPathIndex = 0\n\n                while directoryPathIndex < orderedDirectoryPaths.Length do\n                    let directoryPath = orderedDirectoryPaths[directoryPathIndex]\n\n                    match getParentPath directoryPath with\n                    | Option.Some parentPath ->\n                        let mutable childDirectories = Unchecked.defaultof<ResizeArray<RelativePath>>\n\n                        if\n                            not\n                            <| childDirectoriesByParent.TryGetValue(parentPath, &childDirectories)\n                        then\n                            childDirectories <- ResizeArray<RelativePath>()\n                            childDirectoriesByParent[parentPath] <- childDirectories\n\n                        childDirectories.Add(directoryPath)\n                    | Option.None -> ()\n\n                    directoryPathIndex <- directoryPathIndex + 1\n\n                let computedDirectoryMetadata = Dictionary<RelativePath, DirectoryVersionId * Sha256Hash>(StringComparer.OrdinalIgnoreCase)\n                let directoryVersionsToCreate = ResizeArray<DirectoryVersion>()\n                let mutable materializationError: GraceError option = Option.None\n                let mutable buildIndex = 0\n\n                while buildIndex < orderedDirectoryPaths.Length\n                      && materializationError.IsNone do\n                    let directoryPath = orderedDirectoryPaths[buildIndex]\n                    let mutable childDirectoryPaths = Unchecked.defaultof<ResizeArray<RelativePath>>\n\n                    let childDirectoryPathsArray =\n                        if childDirectoriesByParent.TryGetValue(directoryPath, &childDirectoryPaths) then\n                            childDirectoryPaths\n                            |> Seq.sortBy id\n                            |> Seq.toArray\n                        else\n                            Array.Empty<RelativePath>()\n\n                    let localChildDirectories = List<LocalDirectoryVersion>()\n                    let childDirectoryIds = List<DirectoryVersionId>()\n                    let mutable childIndex = 0\n\n                    while childIndex < childDirectoryPathsArray.Length do\n                        let childDirectoryPath = childDirectoryPathsArray[childIndex]\n                        let childDirectoryId, childSha = computedDirectoryMetadata[childDirectoryPath]\n                        childDirectoryIds.Add(childDirectoryId)\n\n                        localChildDirectories.Add(\n                            LocalDirectoryVersion.Create\n                                childDirectoryId\n                                promotionSetDto.OwnerId\n                                promotionSetDto.OrganizationId\n                                promotionSetDto.RepositoryId\n                                childDirectoryPath\n                                childSha\n                                (List<DirectoryVersionId>())\n                                (List<LocalFileVersion>())\n                                0L\n                                DateTime.UtcNow\n                        )\n\n                        childIndex <- childIndex + 1\n\n                    let directoryFiles = List<FileVersion>()\n                    let localDirectoryFiles = List<LocalFileVersion>()\n                    let mutable filesForDirectory = Unchecked.defaultof<ResizeArray<FileVersion>>\n\n                    if filesByDirectory.TryGetValue(directoryPath, &filesForDirectory) then\n                        let orderedFiles =\n                            filesForDirectory\n                            |> Seq.sortBy (fun fileVersion -> fileVersion.RelativePath)\n                            |> Seq.toArray\n\n                        let mutable orderedFileIndex = 0\n\n                        while orderedFileIndex < orderedFiles.Length do\n                            let fileVersion = orderedFiles[orderedFileIndex]\n                            directoryFiles.Add(fileVersion)\n                            localDirectoryFiles.Add(fileVersion.ToLocalFileVersion DateTime.UtcNow)\n                            orderedFileIndex <- orderedFileIndex + 1\n\n                    let computedSha = computeSha256ForDirectory directoryPath localChildDirectories localDirectoryFiles\n\n                    let tryReuseDirectoryId (snapshot: DirectorySnapshot) =\n                        let mutable existingDirectoryVersion = DirectoryVersion.Default\n\n                        if\n                            snapshot.DirectoriesByPath.TryGetValue(directoryPath, &existingDirectoryVersion)\n                            && existingDirectoryVersion.Sha256Hash = computedSha\n                        then\n                            Option.Some existingDirectoryVersion.DirectoryVersionId\n                        else\n                            Option.None\n\n                    let reusedDirectoryVersionId =\n                        [\n                            oursSnapshot\n                            theirsSnapshot\n                            baseSnapshot\n                        ]\n                        |> Seq.tryPick tryReuseDirectoryId\n\n                    let directoryVersionId =\n                        reusedDirectoryVersionId\n                        |> Option.defaultValue (Guid.NewGuid())\n\n                    if reusedDirectoryVersionId.IsNone then\n                        let directoryVersion =\n                            DirectoryVersion.Create\n                                directoryVersionId\n                                promotionSetDto.OwnerId\n                                promotionSetDto.OrganizationId\n                                promotionSetDto.RepositoryId\n                                directoryPath\n                                computedSha\n                                childDirectoryIds\n                                directoryFiles\n                                (getDirectorySize directoryFiles)\n\n                        directoryVersionsToCreate.Add(directoryVersion)\n\n                    computedDirectoryMetadata[directoryPath] <- (directoryVersionId, computedSha)\n                    buildIndex <- buildIndex + 1\n\n                let mutable createIndex = 0\n\n                while createIndex < directoryVersionsToCreate.Count\n                      && materializationError.IsNone do\n                    let directoryVersion = directoryVersionsToCreate[createIndex]\n\n                    let directoryVersionActorProxy =\n                        DirectoryVersion.CreateActorProxy directoryVersion.DirectoryVersionId promotionSetDto.RepositoryId this.correlationId\n\n                    match!\n                        directoryVersionActorProxy.Handle\n                            (Grace.Types.DirectoryVersion.DirectoryVersionCommand.Create(directoryVersion, repositoryDto))\n                            (this.WithActorMetadata metadata)\n                        with\n                    | Ok _ -> ()\n                    | Error graceError -> materializationError <- Option.Some graceError\n\n                    createIndex <- createIndex + 1\n\n                match materializationError with\n                | Option.Some graceError -> return Error graceError\n                | Option.None ->\n                    let mutable rootDirectory = Unchecked.defaultof<(DirectoryVersionId * Sha256Hash)>\n\n                    if computedDirectoryMetadata.TryGetValue(RootDirectoryPath, &rootDirectory) then\n                        return Ok(fst rootDirectory)\n                    else\n                        return\n                            Error(\n                                (GraceError.Create \"Failed to materialize root DirectoryVersion for PromotionSet recompute.\" metadata.CorrelationId)\n                                    .enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                            )\n            }\n\n        member private this.ComputeAppliedDirectoryVersionForStep\n            (\n                step: PromotionSetStep,\n                computedAgainstBaseDirectoryVersionId: DirectoryVersionId,\n                conflictResolutionPolicy: ConflictResolutionPolicy,\n                manualDecisionsForStep: ConflictResolutionDecision list option,\n                repositoryDto,\n                metadata: EventMetadata\n            ) =\n            task {\n                let! baseSnapshotResult = this.LoadDirectorySnapshot step.OriginalBaseDirectoryVersionId\n\n                match baseSnapshotResult with\n                | Error graceError -> return Error(Failed graceError.Error)\n                | Ok baseSnapshot ->\n                    let! oursSnapshotResult = this.LoadDirectorySnapshot computedAgainstBaseDirectoryVersionId\n\n                    match oursSnapshotResult with\n                    | Error graceError -> return Error(Failed graceError.Error)\n                    | Ok oursSnapshot ->\n                        let! theirsSnapshotResult = this.LoadDirectorySnapshot step.OriginalPromotion.DirectoryVersionId\n\n                        match theirsSnapshotResult with\n                        | Error graceError -> return Error(Failed graceError.Error)\n                        | Ok theirsSnapshot ->\n                            let mergedFilesByPath = Dictionary<RelativePath, FileVersion>(StringComparer.OrdinalIgnoreCase)\n                            let conflicts = ResizeArray<StepConflictFile>()\n                            let allFilePaths = HashSet<RelativePath>(StringComparer.OrdinalIgnoreCase)\n\n                            baseSnapshot.FilesByPath.Keys\n                            |> Seq.iter (fun filePath -> allFilePaths.Add(filePath) |> ignore)\n\n                            oursSnapshot.FilesByPath.Keys\n                            |> Seq.iter (fun filePath -> allFilePaths.Add(filePath) |> ignore)\n\n                            theirsSnapshot.FilesByPath.Keys\n                            |> Seq.iter (fun filePath -> allFilePaths.Add(filePath) |> ignore)\n\n                            let orderedFilePaths = allFilePaths |> Seq.sortBy id |> Seq.toArray\n                            let mutable fileBudgetFailure: RecomputeFailure option = Option.None\n\n                            if orderedFilePaths.Length > maxFilesPerStep then\n                                fileBudgetFailure <-\n                                    Option.Some(Failed($\"Step {step.StepId} exceeded configured file budget ({orderedFilePaths.Length} > {maxFilesPerStep}).\"))\n\n                            let mutable filePathIndex = 0\n\n                            while filePathIndex < orderedFilePaths.Length\n                                  && fileBudgetFailure.IsNone do\n                                let filePath = orderedFilePaths[filePathIndex]\n                                let baseFile = this.TryGetFileVersion(baseSnapshot.FilesByPath, filePath)\n                                let oursFile = this.TryGetFileVersion(oursSnapshot.FilesByPath, filePath)\n                                let theirsFile = this.TryGetFileVersion(theirsSnapshot.FilesByPath, filePath)\n\n                                let oursChanged =\n                                    not\n                                    <| this.FileVersionEquivalent(baseFile, oursFile)\n\n                                let theirsChanged =\n                                    not\n                                    <| this.FileVersionEquivalent(baseFile, theirsFile)\n\n                                let setMergedFile (fileVersion: FileVersion option) =\n                                    match fileVersion with\n                                    | Option.Some selected -> mergedFilesByPath[filePath] <- selected\n                                    | Option.None -> mergedFilesByPath.Remove(filePath) |> ignore\n\n                                if not theirsChanged then\n                                    setMergedFile oursFile\n                                elif not oursChanged then\n                                    setMergedFile theirsFile\n                                elif this.FileVersionEquivalent(oursFile, theirsFile) then\n                                    setMergedFile oursFile\n                                else\n                                    let isBinary =\n                                        match oursFile, theirsFile with\n                                        | Option.Some ours, _\n                                        | _, Option.Some ours -> ours.IsBinary\n                                        | _ -> false\n\n                                    conflicts.Add(\n                                        { FilePath = filePath; BaseFile = baseFile; OursFile = oursFile; TheirsFile = theirsFile; IsBinary = isBinary }\n                                    )\n\n                                filePathIndex <- filePathIndex + 1\n\n                            match fileBudgetFailure with\n                            | Option.Some recomputeFailure -> return Error recomputeFailure\n                            | Option.None when conflicts.Count = 0 ->\n                                match!\n                                    this.MaterializeMergedDirectoryVersion\n                                        (\n                                            repositoryDto,\n                                            baseSnapshot,\n                                            oursSnapshot,\n                                            theirsSnapshot,\n                                            mergedFilesByPath,\n                                            metadata\n                                        )\n                                    with\n                                | Ok appliedDirectoryVersionId -> return Ok(appliedDirectoryVersionId, StepConflictStatus.NoConflicts, Option.None)\n                                | Error graceError -> return Error(Failed graceError.Error)\n                            | Option.None ->\n                                let acceptedDecisionsByPath = Dictionary<RelativePath, ConflictResolutionDecision>(StringComparer.OrdinalIgnoreCase)\n                                let manualAcceptedWithoutOverride = HashSet<RelativePath>(StringComparer.OrdinalIgnoreCase)\n                                let outcomesByPath = Dictionary<RelativePath, ConflictResolutionOutcome>(StringComparer.OrdinalIgnoreCase)\n                                let resolutionMethodByPath = Dictionary<RelativePath, ConflictResolutionMethod>(StringComparer.OrdinalIgnoreCase)\n                                let modelResolvedFilesByPath = Dictionary<RelativePath, FileVersion option>(StringComparer.OrdinalIgnoreCase)\n                                let decisions = manualDecisionsForStep |> Option.defaultValue []\n                                let mutable decisionIndex = 0\n\n                                while decisionIndex < decisions.Length do\n                                    let decision = decisions[decisionIndex]\n\n                                    if decision.Accepted then\n                                        acceptedDecisionsByPath[normalizeFilePath decision.FilePath] <- decision\n\n                                    decisionIndex <- decisionIndex + 1\n\n                                let unresolvedConflicts = ResizeArray<StepConflictFile>()\n                                let conflictsNeedingModel = ResizeArray<StepConflictFile>()\n                                let mutable resolutionError: GraceError option = Option.None\n                                let mutable blockedReason: string option = Option.None\n                                let mutable conflictIndex = 0\n\n                                while conflictIndex < conflicts.Count\n                                      && resolutionError.IsNone\n                                      && blockedReason.IsNone do\n                                    let conflictFile = conflicts[conflictIndex]\n                                    let normalizedFilePath = normalizeFilePath conflictFile.FilePath\n                                    let mutable acceptedDecision = Unchecked.defaultof<ConflictResolutionDecision>\n\n                                    if acceptedDecisionsByPath.TryGetValue(normalizedFilePath, &acceptedDecision) then\n                                        match acceptedDecision.OverrideContentArtifactId with\n                                        | Option.Some artifactId ->\n                                            let! artifactTextResult = this.GetArtifactText(artifactId, metadata)\n\n                                            match artifactTextResult with\n                                            | Error graceError -> resolutionError <- Option.Some graceError\n                                            | Ok artifactText ->\n                                                let! overrideFileVersionResult =\n                                                    this.CreateTextOverrideFileVersion(conflictFile.FilePath, artifactText, repositoryDto, metadata)\n\n                                                match overrideFileVersionResult with\n                                                | Error graceError -> resolutionError <- Option.Some graceError\n                                                | Ok overrideFileVersion ->\n                                                    mergedFilesByPath[conflictFile.FilePath] <- overrideFileVersion\n\n                                                    outcomesByPath[conflictFile.FilePath] <- {\n                                                                                                 ModelResolution = \"Manual override artifact accepted.\"\n                                                                                                 Confidence = 1.0\n                                                                                                 Accepted = Option.Some true\n                                                                                             }\n\n                                                    resolutionMethodByPath[conflictFile.FilePath] <- ConflictResolutionMethod.ManualOverride\n                                        | Option.None ->\n                                            if conflictFile.IsBinary then\n                                                blockedReason <- Option.Some \"Binary file conflicts require manual override content.\"\n                                            else\n                                                manualAcceptedWithoutOverride.Add(conflictFile.FilePath)\n                                                |> ignore\n\n                                                conflictsNeedingModel.Add(conflictFile)\n                                    else\n                                        unresolvedConflicts.Add(conflictFile)\n\n                                        if not conflictFile.IsBinary then conflictsNeedingModel.Add(conflictFile)\n\n                                    conflictIndex <- conflictIndex + 1\n\n                                match resolutionError with\n                                | Option.Some graceError -> return Error(Failed graceError.Error)\n                                | Option.None ->\n                                    let hasUnresolvedConflicts = unresolvedConflicts.Count > 0\n\n                                    let hasUnresolvedBinaryConflict =\n                                        unresolvedConflicts\n                                        |> Seq.exists (fun conflict -> conflict.IsBinary)\n\n                                    let mutable confidence: float option = Option.None\n                                    let mutable appliedByPolicy = false\n                                    let mutable minAutoResolvedConfidence = 1.0\n\n                                    if blockedReason.IsNone\n                                       && hasUnresolvedBinaryConflict then\n                                        blockedReason <- Option.Some \"Binary file conflicts require manual override and cannot be auto-merged.\"\n\n                                    if blockedReason.IsNone && hasUnresolvedConflicts then\n                                        match conflictResolutionPolicy with\n                                        | ConflictResolutionPolicy.NoConflicts _ ->\n                                            blockedReason <- Option.Some $\"Conflict detected at step {step.StepId}, and repository policy is NoConflicts.\"\n                                        | ConflictResolutionPolicy.ConflictsAllowed _ -> ()\n\n                                    if blockedReason.IsNone\n                                       && conflictsNeedingModel.Count > 0 then\n                                        let mutable modelFailure: string option = Option.None\n                                        let mutable modelIndex = 0\n\n                                        while modelIndex < conflictsNeedingModel.Count\n                                              && modelFailure.IsNone do\n                                            let conflictFile = conflictsNeedingModel[modelIndex]\n\n                                            let! modelResolutionResult = this.ResolveConflictWithModel(repositoryDto, conflictFile, metadata)\n\n                                            match modelResolutionResult with\n                                            | Error errorText ->\n                                                modelFailure <-\n                                                    Option.Some(\n                                                        sprintf\n                                                            \"Model resolution failed at step %O for file '%s': %s\"\n                                                            step.StepId\n                                                            conflictFile.FilePath\n                                                            errorText\n                                                    )\n                                            | Ok (modelOutcome, resolvedFileVersion) ->\n                                                outcomesByPath[conflictFile.FilePath] <- modelOutcome\n                                                modelResolvedFilesByPath[conflictFile.FilePath] <- resolvedFileVersion\n\n                                                if manualAcceptedWithoutOverride.Contains(conflictFile.FilePath) then\n                                                    outcomesByPath[conflictFile.FilePath] <- { modelOutcome with Accepted = Option.Some true }\n\n                                                    resolutionMethodByPath[conflictFile.FilePath] <- ConflictResolutionMethod.ManualOverride\n\n                                                    match resolvedFileVersion with\n                                                    | Option.Some fileVersion -> mergedFilesByPath[conflictFile.FilePath] <- fileVersion\n                                                    | Option.None ->\n                                                        mergedFilesByPath.Remove(conflictFile.FilePath)\n                                                        |> ignore\n                                                else\n                                                    resolutionMethodByPath[conflictFile.FilePath] <- ConflictResolutionMethod.ModelSuggested\n\n                                            modelIndex <- modelIndex + 1\n\n                                        match modelFailure with\n                                        | Option.Some reason -> blockedReason <- Option.Some reason\n                                        | Option.None -> ()\n\n                                    if blockedReason.IsNone && hasUnresolvedConflicts then\n                                        match conflictResolutionPolicy with\n                                        | ConflictResolutionPolicy.NoConflicts _ -> ()\n                                        | ConflictResolutionPolicy.ConflictsAllowed threshold ->\n                                            let mutable unresolvedIndex = 0\n\n                                            while unresolvedIndex < unresolvedConflicts.Count\n                                                  && blockedReason.IsNone do\n                                                let unresolvedConflict = unresolvedConflicts[unresolvedIndex]\n                                                let mutable modelOutcome = Unchecked.defaultof<ConflictResolutionOutcome>\n\n                                                if outcomesByPath.TryGetValue(unresolvedConflict.FilePath, &modelOutcome) then\n                                                    if modelOutcome.Confidence >= float threshold then\n                                                        outcomesByPath[unresolvedConflict.FilePath] <- { modelOutcome with Accepted = Option.Some true }\n\n                                                        minAutoResolvedConfidence <- min minAutoResolvedConfidence modelOutcome.Confidence\n                                                        appliedByPolicy <- true\n\n                                                        let mutable resolvedFileVersion = Unchecked.defaultof<FileVersion option>\n\n                                                        if modelResolvedFilesByPath.TryGetValue(unresolvedConflict.FilePath, &resolvedFileVersion) then\n                                                            match resolvedFileVersion with\n                                                            | Option.Some fileVersion -> mergedFilesByPath[unresolvedConflict.FilePath] <- fileVersion\n                                                            | Option.None ->\n                                                                mergedFilesByPath.Remove(unresolvedConflict.FilePath)\n                                                                |> ignore\n                                                        else\n                                                            blockedReason <-\n                                                                Option.Some(\n                                                                    sprintf\n                                                                        \"Model resolution did not return content for conflicted file '%s' at step %O.\"\n                                                                        unresolvedConflict.FilePath\n                                                                        step.StepId\n                                                                )\n                                                    else\n                                                        blockedReason <-\n                                                            Option.Some(\n                                                                sprintf\n                                                                    \"Conflict confidence %.2f is below threshold %.2f at step %O.\"\n                                                                    modelOutcome.Confidence\n                                                                    (float threshold)\n                                                                    step.StepId\n                                                            )\n                                                else\n                                                    blockedReason <-\n                                                        Option.Some(\n                                                            sprintf\n                                                                \"Model resolution did not return a proposal for conflicted file '%s' at step %O.\"\n                                                                unresolvedConflict.FilePath\n                                                                step.StepId\n                                                        )\n\n                                                unresolvedIndex <- unresolvedIndex + 1\n\n                                            if appliedByPolicy then confidence <- Option.Some minAutoResolvedConfidence\n\n                                    let conflictFilesForArtifact = conflicts |> Seq.toList\n\n                                    let! conflictAnalysesResult =\n                                        this.BuildConflictAnalyses(repositoryDto, conflictFilesForArtifact, outcomesByPath, resolutionMethodByPath)\n\n                                    match conflictAnalysesResult with\n                                    | Error graceError -> return Error(Failed graceError.Error)\n                                    | Ok conflictAnalyses ->\n                                        match blockedReason with\n                                        | Option.Some reasonText ->\n                                            let! artifactId =\n                                                this.CreateConflictArtifact(\n                                                    step,\n                                                    reasonText,\n                                                    confidence,\n                                                    computedAgainstBaseDirectoryVersionId,\n                                                    metadata,\n                                                    manualDecisionsForStep,\n                                                    Option.Some conflictAnalyses\n                                                )\n\n                                            return Error(Blocked(reasonText, artifactId))\n                                        | Option.None ->\n                                            let resolutionReason =\n                                                if appliedByPolicy && confidence.IsSome then\n                                                    sprintf \"Conflicts auto-resolved by policy at confidence %.2f.\" confidence.Value\n                                                elif manualAcceptedWithoutOverride.Count > 0 then\n                                                    \"Conflicts resolved through manual review decisions.\"\n                                                else\n                                                    \"Conflicts resolved.\"\n\n                                            let! conflictArtifactId =\n                                                this.CreateConflictArtifact(\n                                                    step,\n                                                    resolutionReason,\n                                                    confidence,\n                                                    computedAgainstBaseDirectoryVersionId,\n                                                    metadata,\n                                                    manualDecisionsForStep,\n                                                    Option.Some conflictAnalyses\n                                                )\n\n                                            match!\n                                                this.MaterializeMergedDirectoryVersion\n                                                    (\n                                                        repositoryDto,\n                                                        baseSnapshot,\n                                                        oursSnapshot,\n                                                        theirsSnapshot,\n                                                        mergedFilesByPath,\n                                                        metadata\n                                                    )\n                                                with\n                                            | Ok appliedDirectoryVersionId ->\n                                                return Ok(appliedDirectoryVersionId, StepConflictStatus.AutoResolved, conflictArtifactId)\n                                            | Error graceError -> return Error(Failed graceError.Error)\n            }\n\n        member private this.CreateConflictArtifact\n            (\n                step: PromotionSetStep,\n                reason: string,\n                confidence: float option,\n                computedAgainstBaseDirectoryVersionId: DirectoryVersionId,\n                metadata: EventMetadata,\n                manualDecisions: ConflictResolutionDecision list option,\n                conflictAnalyses: ConflictAnalysis list option\n            ) =\n            task {\n                try\n                    let defaultConflictAnalysis: ConflictAnalysis =\n                        {\n                            FilePath = \"__step__\"\n                            OriginalHunks =\n                                [\n                                    {\n                                        StartLine = 1\n                                        EndLine = 1\n                                        OursContent = $\"{computedAgainstBaseDirectoryVersionId}\"\n                                        TheirsContent = $\"{step.OriginalPromotion.DirectoryVersionId}\"\n                                    }\n                                ]\n                            ProposedResolution = Option.None\n                            ResolutionMethod = ConflictResolutionMethod.None\n                        }\n\n                    let report =\n                        {|\n                            promotionSetId = promotionSetDto.PromotionSetId\n                            targetBranchId = promotionSetDto.TargetBranchId\n                            stepId = step.StepId\n                            order = step.Order\n                            reason = reason\n                            confidence = confidence\n                            computedAgainstBaseDirectoryVersionId = computedAgainstBaseDirectoryVersionId\n                            originalBaseDirectoryVersionId = step.OriginalBaseDirectoryVersionId\n                            originalPromotionDirectoryVersionId = step.OriginalPromotion.DirectoryVersionId\n                            conflicts =\n                                conflictAnalyses\n                                |> Option.defaultValue [ defaultConflictAnalysis ]\n                            manualDecisions =\n                                (manualDecisions\n                                 |> Option.defaultValue []\n                                 |> List.map (fun decision ->\n                                     {|\n                                         filePath = decision.FilePath\n                                         accepted = decision.Accepted\n                                         overrideContentArtifactId = decision.OverrideContentArtifactId\n                                     |}))\n                        |}\n\n                    let reportJson = serialize report\n                    let artifactId: ArtifactId = Guid.NewGuid()\n                    let nowUtc = getCurrentInstant().InUtc()\n\n                    let blobPath = sprintf \"grace-artifacts/%04i/%02i/%02i/%02i/%O\" nowUtc.Year nowUtc.Month nowUtc.Day nowUtc.Hour artifactId\n\n                    let artifactMetadata: ArtifactMetadata =\n                        { ArtifactMetadata.Default with\n                            ArtifactId = artifactId\n                            OwnerId = promotionSetDto.OwnerId\n                            OrganizationId = promotionSetDto.OrganizationId\n                            RepositoryId = promotionSetDto.RepositoryId\n                            ArtifactType = ArtifactType.ConflictReport\n                            MimeType = \"application/json\"\n                            Size = int64 reportJson.Length\n                            BlobPath = blobPath\n                            CreatedAt = getCurrentInstant ()\n                            CreatedBy = UserId metadata.Principal\n                        }\n\n                    let artifactActorProxy = Artifact.CreateActorProxy artifactId promotionSetDto.RepositoryId this.correlationId\n\n                    match! artifactActorProxy.Handle (ArtifactCommand.Create artifactMetadata) (this.WithActorMetadata metadata) with\n                    | Ok _ ->\n                        match! this.UploadArtifactPayload(blobPath, reportJson, metadata) with\n                        | Ok () -> return Option.Some artifactId\n                        | Error uploadError ->\n                            log.LogWarning(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Conflict artifact metadata was created but payload upload failed for PromotionSetId {PromotionSetId}; ArtifactId {ArtifactId}. Error: {GraceError}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                metadata.CorrelationId,\n                                promotionSetDto.PromotionSetId,\n                                artifactId,\n                                uploadError\n                            )\n\n                            return Option.Some artifactId\n                    | Error graceError ->\n                        log.LogWarning(\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to persist conflict artifact metadata for PromotionSetId {PromotionSetId}. Error: {GraceError}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            metadata.CorrelationId,\n                            promotionSetDto.PromotionSetId,\n                            graceError\n                        )\n\n                        return Option.None\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Exception while creating conflict artifact for PromotionSetId {PromotionSetId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        metadata.CorrelationId,\n                        promotionSetDto.PromotionSetId\n                    )\n\n                    return Option.None\n            }\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            promotionSetDto <-\n                state.State\n                |> Seq.fold (fun dto event -> PromotionSetDto.UpdateDto event dto) promotionSetDto\n\n            Task.CompletedTask\n\n        interface IGraceReminderWithGuidKey with\n            member this.ScheduleReminderAsync reminderType delay reminderState correlationId =\n                task {\n                    let reminderDto =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            promotionSetDto.OwnerId\n                            promotionSetDto.OrganizationId\n                            promotionSetDto.RepositoryId\n                            reminderType\n                            (getFutureInstant delay)\n                            reminderState\n                            correlationId\n\n                    do! createReminder reminderDto\n                }\n                :> Task\n\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    this.correlationId <- reminder.CorrelationId\n\n                    return\n                        Error(\n                            (GraceError.Create\n                                $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminder.ReminderType} with state {getDiscriminatedUnionCaseName reminder.State}.\"\n                                this.correlationId)\n                                .enhance (\"IsRetryable\", \"false\")\n                        )\n                }\n\n        member private this.ApplyEvent(promotionSetEvent: PromotionSetEvent) =\n            task {\n                let normalizedMetadata = this.WithActorMetadata promotionSetEvent.Metadata\n                let normalizedEvent = { promotionSetEvent with Metadata = normalizedMetadata }\n                let correlationId = normalizedMetadata.CorrelationId\n\n                try\n                    state.State.Add(normalizedEvent)\n                    do! state.WriteStateAsync()\n\n                    promotionSetDto <-\n                        promotionSetDto\n                        |> PromotionSetDto.UpdateDto normalizedEvent\n\n                    let graceEvent = GraceEvent.PromotionSetEvent normalizedEvent\n                    do! publishGraceEvent graceEvent normalizedMetadata\n                    return this.BuildSuccess(\"Promotion set command succeeded.\", correlationId)\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to apply event for PromotionSetId: {PromotionSetId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        promotionSetDto.PromotionSetId\n                    )\n\n                    return\n                        Error(\n                            (GraceError.CreateWithException ex \"Failed while applying PromotionSet event.\" correlationId)\n                                .enhance(nameof RepositoryId, promotionSetDto.RepositoryId)\n                                .enhance (nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                        )\n            }\n\n        member private this.RecomputeSteps\n            (\n                metadata: EventMetadata,\n                reason: string option,\n                manualResolution: (PromotionSetStepId * ConflictResolutionDecision list) option\n            ) =\n            task {\n                if promotionSetDto.StepsComputationStatus = StepsComputationStatus.Computing then\n                    return this.BuildError(\"PromotionSet steps are already computing.\", metadata.CorrelationId)\n                else\n                    let! currentTerminalReferenceId, targetBaseDirectoryVersionId = this.GetCurrentTerminalPromotion()\n\n                    if manualResolution.IsNone\n                       && promotionSetDto.StepsComputationStatus = StepsComputationStatus.Computed\n                       && promotionSetDto.ComputedAgainstParentTerminalPromotionReferenceId = Option.Some currentTerminalReferenceId then\n                        return this.BuildSuccess(\"PromotionSet steps are already computed against the current terminal promotion.\", metadata.CorrelationId)\n                    else\n                        match! this.ApplyEvent { Event = PromotionSetEventType.RecomputeStarted currentTerminalReferenceId; Metadata = metadata } with\n                        | Error graceError -> return Error graceError\n                        | Ok _ ->\n                            let! conflictResolutionPolicy = this.GetConflictResolutionPolicy()\n                            let! repositoryDto = this.GetRepositoryDto()\n\n                            let orderedSteps =\n                                promotionSetDto.Steps\n                                |> List.sortBy (fun step -> step.Order)\n\n                            let computedSteps = ResizeArray<PromotionSetStep>()\n                            let mutable recomputeFailure: RecomputeFailure option = Option.None\n                            let mutable currentHeadDirectoryVersionId = targetBaseDirectoryVersionId\n                            let mutable index = 0\n                            let recomputeStopwatch = Stopwatch.StartNew()\n\n                            if orderedSteps.Length > maxStepsPerRecompute then\n                                recomputeFailure <-\n                                    Option.Some(Failed($\"Recompute exceeded configured step budget ({orderedSteps.Length} > {maxStepsPerRecompute}).\"))\n\n                            while index < orderedSteps.Length\n                                  && recomputeFailure.IsNone do\n                                if recomputeStopwatch.ElapsedMilliseconds > int64 maxTotalTimeMilliseconds then\n                                    recomputeFailure <-\n                                        Option.Some(\n                                            Failed(\n                                                $\"Recompute exceeded total time budget ({recomputeStopwatch.ElapsedMilliseconds} ms > {maxTotalTimeMilliseconds} ms).\"\n                                            )\n                                        )\n\n                                let currentStep = orderedSteps[index]\n                                let stepStopwatch = Stopwatch.StartNew()\n                                let! hydratedStepResult = this.HydrateStepProvenance currentStep\n\n                                match hydratedStepResult with\n                                | Error graceError -> recomputeFailure <- Option.Some(Failed graceError.Error)\n                                | Ok hydratedStep ->\n                                    let computedAgainstBaseDirectoryVersionId = currentHeadDirectoryVersionId\n\n                                    let manualDecisionsForStep =\n                                        match manualResolution with\n                                        | Option.Some (resolvedStepId, decisions) when resolvedStepId = hydratedStep.StepId -> Option.Some decisions\n                                        | _ -> Option.None\n\n                                    let hasManualOverride =\n                                        manualDecisionsForStep\n                                        |> Option.defaultValue []\n                                        |> List.exists (fun decision ->\n                                            decision.Accepted\n                                            && decision.OverrideContentArtifactId.IsSome)\n\n                                    let! manualOverrideValidation =\n                                        if hasManualOverride then\n                                            this.ValidateManualOverrideArtifacts(manualDecisionsForStep |> Option.defaultValue [], metadata)\n                                        else\n                                            Task.FromResult(Ok())\n\n                                    match manualOverrideValidation with\n                                    | Error graceError -> recomputeFailure <- Option.Some(Failed graceError.Error)\n                                    | Ok () ->\n                                        let! stepComputationResult =\n                                            this.ComputeAppliedDirectoryVersionForStep(\n                                                hydratedStep,\n                                                computedAgainstBaseDirectoryVersionId,\n                                                conflictResolutionPolicy,\n                                                manualDecisionsForStep,\n                                                repositoryDto,\n                                                metadata\n                                            )\n\n                                        match stepComputationResult with\n                                        | Ok (appliedDirectoryVersionId, conflictStatus, conflictArtifactId) ->\n                                            let computedStep =\n                                                { hydratedStep with\n                                                    ComputedAgainstBaseDirectoryVersionId = computedAgainstBaseDirectoryVersionId\n                                                    AppliedDirectoryVersionId = appliedDirectoryVersionId\n                                                    ConflictSummaryArtifactId = conflictArtifactId\n                                                    ConflictStatus = conflictStatus\n                                                }\n\n                                            computedSteps.Add(computedStep)\n                                            currentHeadDirectoryVersionId <- computedStep.AppliedDirectoryVersionId\n                                        | Error stepFailure -> recomputeFailure <- Option.Some stepFailure\n\n                                if recomputeFailure.IsNone\n                                   && stepStopwatch.ElapsedMilliseconds > int64 maxStepTimeMilliseconds then\n                                    recomputeFailure <-\n                                        Option.Some(\n                                            Failed(\n                                                $\"Step {currentStep.StepId} exceeded step time budget ({stepStopwatch.ElapsedMilliseconds} ms > {maxStepTimeMilliseconds} ms).\"\n                                            )\n                                        )\n\n                                index <- index + 1\n\n                            match recomputeFailure with\n                            | Option.None ->\n                                match!\n                                    this.ApplyEvent\n                                        {\n                                            Event = PromotionSetEventType.StepsUpdated(computedSteps |> Seq.toList, currentTerminalReferenceId)\n                                            Metadata = metadata\n                                        }\n                                    with\n                                | Ok graceReturnValue -> return Ok graceReturnValue\n                                | Error graceError -> return Error graceError\n                            | Option.Some (Blocked (reasonText, artifactId)) ->\n                                match! this.ApplyEvent { Event = PromotionSetEventType.Blocked(reasonText, artifactId); Metadata = metadata } with\n                                | Ok _ -> return this.BuildError(reasonText, metadata.CorrelationId)\n                                | Error graceError -> return Error graceError\n                            | Option.Some (Failed reasonText) ->\n                                match!\n                                    this.ApplyEvent\n                                        { Event = PromotionSetEventType.RecomputeFailed(reasonText, currentTerminalReferenceId); Metadata = metadata }\n                                    with\n                                | Ok _ -> return this.BuildError(reasonText, metadata.CorrelationId)\n                                | Error graceError -> return Error graceError\n            }\n\n        member private this.RollbackCreatedPromotions(createdReferenceIds: List<ReferenceId>, rollbackReason: string, metadata: EventMetadata) =\n            task {\n                let mutable index = 0\n\n                while index < createdReferenceIds.Count do\n                    let referenceId = createdReferenceIds[index]\n                    let referenceActorProxy = Reference.CreateActorProxy referenceId promotionSetDto.RepositoryId this.correlationId\n                    let rollbackMetadata = this.WithActorMetadata metadata\n                    rollbackMetadata.Properties[ \"ActorId\" ] <- $\"{referenceId}\"\n\n                    match! referenceActorProxy.Handle (ReferenceCommand.DeleteLogical(true, rollbackReason)) rollbackMetadata with\n                    | Ok _ -> ()\n                    | Error graceError ->\n                        log.LogWarning(\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to rollback reference {ReferenceId} for PromotionSetId {PromotionSetId}. Error: {GraceError}\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            metadata.CorrelationId,\n                            referenceId,\n                            promotionSetDto.PromotionSetId,\n                            graceError\n                        )\n\n                    index <- index + 1\n\n                let branchActorProxy = Branch.CreateActorProxy promotionSetDto.TargetBranchId promotionSetDto.RepositoryId this.correlationId\n                do! branchActorProxy.MarkForRecompute metadata.CorrelationId\n            }\n\n        member private this.CreatePromotionReference(step: PromotionSetStep, isTerminal: bool, metadata: EventMetadata) =\n            task {\n                let directoryVersionActorProxy =\n                    DirectoryVersion.CreateActorProxy step.AppliedDirectoryVersionId promotionSetDto.RepositoryId this.correlationId\n\n                let! directoryVersionDto = directoryVersionActorProxy.Get this.correlationId\n\n                if directoryVersionDto.DirectoryVersion.DirectoryVersionId = DirectoryVersionId.Empty then\n                    return\n                        Error(\n                            (GraceError.Create \"Applied directory version does not exist.\" metadata.CorrelationId)\n                                .enhance(nameof PromotionSetStepId, step.StepId)\n                                .enhance (nameof DirectoryVersionId, step.AppliedDirectoryVersionId)\n                        )\n                else\n                    let referenceId: ReferenceId = Guid.NewGuid()\n                    let links = ResizeArray<ReferenceLinkType>()\n                    links.Add(ReferenceLinkType.IncludedInPromotionSet promotionSetDto.PromotionSetId)\n\n                    if isTerminal then\n                        links.Add(ReferenceLinkType.PromotionSetTerminal promotionSetDto.PromotionSetId)\n\n                    let referenceMetadata = this.WithActorMetadata metadata\n                    referenceMetadata.Properties[ \"ActorId\" ] <- $\"{referenceId}\"\n                    referenceMetadata.Properties[ nameof BranchId ] <- $\"{promotionSetDto.TargetBranchId}\"\n                    let referenceActorProxy = Reference.CreateActorProxy referenceId promotionSetDto.RepositoryId this.correlationId\n                    let referenceText = ReferenceText $\"PromotionSet {promotionSetDto.PromotionSetId} Step {step.Order}\"\n\n                    let referenceCommand =\n                        ReferenceCommand.Create(\n                            referenceId,\n                            promotionSetDto.OwnerId,\n                            promotionSetDto.OrganizationId,\n                            promotionSetDto.RepositoryId,\n                            promotionSetDto.TargetBranchId,\n                            step.AppliedDirectoryVersionId,\n                            directoryVersionDto.DirectoryVersion.Sha256Hash,\n                            ReferenceType.Promotion,\n                            referenceText,\n                            links\n                        )\n\n                    match! referenceActorProxy.Handle referenceCommand referenceMetadata with\n                    | Ok _ -> return Ok referenceId\n                    | Error graceError -> return Error graceError\n            }\n\n        member private this.GetRequiredValidationsForApply() =\n            task {\n                let! validationSets = getValidationSets promotionSetDto.RepositoryId 500 false this.correlationId\n\n                return\n                    validationSets\n                    |> List.filter (fun validationSet -> validationSet.TargetBranchId = promotionSetDto.TargetBranchId)\n                    |> List.collect (fun validationSet -> validationSet.Validations)\n                    |> List.filter (fun validation -> validation.RequiredForApply)\n                    |> List.distinctBy (fun validation -> $\"{validation.Name.Trim().ToLowerInvariant()}::{validation.Version.Trim().ToLowerInvariant()}\")\n            }\n\n        member private this.EnsureRequiredValidationsPass(metadata: EventMetadata) =\n            task {\n                let! requiredValidations = this.GetRequiredValidationsForApply()\n\n                if requiredValidations.IsEmpty then\n                    return Ok()\n                else\n                    let! scopedValidationResults =\n                        getValidationResultsForPromotionSetAttempt\n                            promotionSetDto.RepositoryId\n                            promotionSetDto.PromotionSetId\n                            promotionSetDto.StepsComputationAttempt\n                            5000\n                            this.correlationId\n\n                    let hasPass (validationName: string) (validationVersion: string) =\n                        scopedValidationResults\n                        |> List.exists (fun validationResult ->\n                            String.Equals(validationResult.ValidationName, validationName, StringComparison.OrdinalIgnoreCase)\n                            && String.Equals(validationResult.ValidationVersion, validationVersion, StringComparison.OrdinalIgnoreCase)\n                            && validationResult.Output.Status = ValidationStatus.Pass)\n\n                    let missingOrFailing =\n                        requiredValidations\n                        |> List.filter (fun validation -> not <| hasPass validation.Name validation.Version)\n\n                    if missingOrFailing.IsEmpty then\n                        return Ok()\n                    else\n                        let details =\n                            missingOrFailing\n                            |> List.map (fun validation -> $\"{validation.Name}:{validation.Version}\")\n                            |> String.concat \", \"\n\n                        return\n                            Error(\n                                (GraceError.Create\n                                    $\"Required validations have not passed for StepsComputationAttempt {promotionSetDto.StepsComputationAttempt}: {details}.\"\n                                    metadata.CorrelationId)\n                                    .enhance(nameof PromotionSetId, promotionSetDto.PromotionSetId)\n                                    .enhance (\"StepsComputationAttempt\", promotionSetDto.StepsComputationAttempt)\n                            )\n            }\n\n        member private this.ApplyPromotionSet(metadata: EventMetadata) =\n            task {\n                if promotionSetDto.Status = PromotionSetStatus.Succeeded then\n                    return this.BuildError(\"PromotionSet has already been applied successfully.\", metadata.CorrelationId)\n                elif promotionSetDto.DeletedAt.IsSome then\n                    return this.BuildError(\"PromotionSet has been deleted and cannot be applied.\", metadata.CorrelationId)\n                else\n                    let! currentTerminalReferenceId, _ = this.GetCurrentTerminalPromotion()\n\n                    let needsRecompute =\n                        promotionSetDto.StepsComputationStatus\n                        <> StepsComputationStatus.Computed\n                        || promotionSetDto.ComputedAgainstParentTerminalPromotionReferenceId\n                           <> Option.Some currentTerminalReferenceId\n\n                    let mutable recomputeError: GraceError option = Option.None\n\n                    if needsRecompute then\n                        let! recomputeResult = this.RecomputeSteps(metadata, Option.Some \"Apply requested on stale or uncomputed steps.\", Option.None)\n\n                        match recomputeResult with\n                        | Ok _ -> ()\n                        | Error graceError -> recomputeError <- Option.Some graceError\n\n                    let mutable preconditionError: GraceError option = recomputeError\n\n                    if preconditionError.IsNone\n                       && promotionSetDto.StepsComputationStatus\n                          <> StepsComputationStatus.Computed then\n                        preconditionError <- Option.Some(GraceError.Create \"PromotionSet steps are not computed.\" metadata.CorrelationId)\n\n                    if preconditionError.IsNone\n                       && promotionSetDto.Steps.IsEmpty then\n                        preconditionError <- Option.Some(GraceError.Create \"PromotionSet does not have any steps to apply.\" metadata.CorrelationId)\n\n                    if preconditionError.IsNone then\n                        match! this.EnsureRequiredValidationsPass metadata with\n                        | Ok _ -> ()\n                        | Error graceError -> preconditionError <- Option.Some graceError\n\n                    if preconditionError.IsSome then\n                        return Error preconditionError.Value\n                    else\n                        match! this.ApplyEvent { Event = PromotionSetEventType.ApplyStarted; Metadata = metadata } with\n                        | Error graceError -> return Error graceError\n                        | Ok _ ->\n                            let createdReferenceIds = List<ReferenceId>()\n\n                            let orderedSteps =\n                                promotionSetDto.Steps\n                                |> List.sortBy (fun step -> step.Order)\n\n                            let mutable applyError: GraceError option = Option.None\n                            let mutable index = 0\n\n                            while index < orderedSteps.Length && applyError.IsNone do\n                                let step = orderedSteps[index]\n                                let isTerminal = index = (orderedSteps.Length - 1)\n                                let! createReferenceResult = this.CreatePromotionReference(step, isTerminal, metadata)\n\n                                match createReferenceResult with\n                                | Ok referenceId -> createdReferenceIds.Add(referenceId)\n                                | Error graceError -> applyError <- Option.Some graceError\n\n                                index <- index + 1\n\n                            match applyError with\n                            | Option.None ->\n                                let branchActorProxy = Branch.CreateActorProxy promotionSetDto.TargetBranchId promotionSetDto.RepositoryId this.correlationId\n                                do! branchActorProxy.MarkForRecompute metadata.CorrelationId\n                                let terminalReferenceId = createdReferenceIds[createdReferenceIds.Count - 1]\n\n                                match! this.ApplyEvent { Event = PromotionSetEventType.Applied terminalReferenceId; Metadata = metadata } with\n                                | Error graceError -> return Error graceError\n                                | Ok graceReturnValue ->\n                                    let queueActorProxy =\n                                        PromotionQueue.CreateActorProxy promotionSetDto.TargetBranchId promotionSetDto.RepositoryId this.correlationId\n\n                                    let! queueExists = queueActorProxy.Exists metadata.CorrelationId\n\n                                    if queueExists then\n                                        let dequeueMetadata = this.WithActorMetadata metadata\n\n                                        match! queueActorProxy.Handle (PromotionQueueCommand.Dequeue promotionSetDto.PromotionSetId) dequeueMetadata with\n                                        | Ok _ -> ()\n                                        | Error graceError ->\n                                            log.LogWarning(\n                                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to dequeue PromotionSetId {PromotionSetId} after apply. Error: {GraceError}\",\n                                                getCurrentInstantExtended (),\n                                                getMachineName,\n                                                metadata.CorrelationId,\n                                                promotionSetDto.PromotionSetId,\n                                                graceError\n                                            )\n\n                                    return Ok graceReturnValue\n                            | Option.Some graceError ->\n                                do!\n                                    this.RollbackCreatedPromotions(\n                                        createdReferenceIds,\n                                        \"PromotionSet apply failed. Rolling back previously created references.\",\n                                        metadata\n                                    )\n\n                                match! this.ApplyEvent { Event = PromotionSetEventType.ApplyFailed graceError.Error; Metadata = metadata } with\n                                | Ok _ -> return Error graceError\n                                | Error applyFailureError -> return Error applyFailureError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = promotionSetDto.RepositoryId |> returnTask\n\n        interface IPromotionSetActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                not\n                <| promotionSetDto.PromotionSetId.Equals(PromotionSetId.Empty)\n                |> returnTask\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                promotionSetDto.DeletedAt.IsSome |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                promotionSetDto |> returnTask\n\n            member this.GetEvents correlationId =\n                this.correlationId <- correlationId\n\n                state.State :> IReadOnlyList<PromotionSetEvent>\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (promotionSetCommand: PromotionSetCommand) (eventMetadata: EventMetadata) =\n                    task { return validateCommandForState state.State promotionSetDto promotionSetCommand eventMetadata }\n\n                let processCommand (promotionSetCommand: PromotionSetCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        match promotionSetCommand with\n                        | PromotionSetCommand.CreatePromotionSet (promotionSetId, ownerId, organizationId, repositoryId, targetBranchId) ->\n                            return!\n                                this.ApplyEvent\n                                    {\n                                        Event = PromotionSetEventType.Created(promotionSetId, ownerId, organizationId, repositoryId, targetBranchId)\n                                        Metadata = eventMetadata\n                                    }\n                        | PromotionSetCommand.UpdateInputPromotions promotionPointers ->\n                            match! this.ApplyEvent { Event = PromotionSetEventType.InputPromotionsUpdated promotionPointers; Metadata = eventMetadata } with\n                            | Error graceError -> return Error graceError\n                            | Ok _ -> return! this.RecomputeSteps(eventMetadata, Option.Some \"Input promotions changed.\", Option.None)\n                        | PromotionSetCommand.RecomputeStepsIfStale reason -> return! this.RecomputeSteps(eventMetadata, reason, Option.None)\n                        | PromotionSetCommand.ResolveConflicts (stepId, resolutions) ->\n                            return! this.RecomputeSteps(eventMetadata, Option.Some \"Manual conflict resolutions submitted.\", Option.Some(stepId, resolutions))\n                        | PromotionSetCommand.Apply -> return! this.ApplyPromotionSet eventMetadata\n                        | PromotionSetCommand.DeleteLogical (force, deleteReason) ->\n                            return! this.ApplyEvent { Event = PromotionSetEventType.LogicalDeleted(force, deleteReason); Metadata = eventMetadata }\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok validCommand -> return! processCommand validCommand metadata\n                    | Error validationError -> return Error validationError\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Reference.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Timing\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Events\nopen Grace.Types.Reference\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Globalization\nopen System.Threading.Tasks\n\nmodule Reference =\n\n    type ReferenceActor([<PersistentState(StateName.Reference, Constants.GraceActorStorage)>] state: IPersistentState<List<ReferenceEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Reference\n\n        let log = loggerFactory.CreateLogger(\"Reference.Actor\")\n\n        let mutable currentCommand = String.Empty\n\n        let mutable referenceDto = ReferenceDto.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            referenceDto <-\n                state.State\n                |> Seq.fold (fun referenceDto event -> ReferenceDto.UpdateDto event referenceDto) referenceDto\n\n            Task.CompletedTask\n\n        interface IGraceReminderWithGuidKey with\n            /// Schedules a Grace reminder.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminderDto =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            referenceDto.OwnerId\n                            referenceDto.OrganizationId\n                            referenceDto.RepositoryId\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminderDto\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    this.correlationId <- reminder.CorrelationId\n\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.PhysicalDeletion, ReminderState.ReferencePhysicalDeletion physicalDeletionReminderState ->\n\n                        this.correlationId <- physicalDeletionReminderState.CorrelationId\n\n                        // Mark the branch as needing to update its latest references.\n                        let branchActorProxy =\n                            Branch.CreateActorProxy physicalDeletionReminderState.BranchId physicalDeletionReminderState.RepositoryId this.correlationId\n\n                        do! branchActorProxy.MarkForRecompute physicalDeletionReminderState.CorrelationId\n\n                        // Delete saved state for this actor.\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Deleted physical state for reference; RepositoryId: {RepositoryId}; BranchId: {BranchId}; ReferenceId: {ReferenceId}; DirectoryVersionId: {DirectoryVersionId}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            physicalDeletionReminderState.CorrelationId,\n                            physicalDeletionReminderState.RepositoryId,\n                            physicalDeletionReminderState.BranchId,\n                            referenceDto.ReferenceId,\n                            physicalDeletionReminderState.DirectoryVersionId,\n                            physicalDeletionReminderState.DeleteReason\n                        )\n\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                (GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId)\n                                    .enhance (\"IsRetryable\", \"false\")\n                            )\n                }\n\n        member private this.ApplyEvent(referenceEvent: ReferenceEvent) =\n            task {\n                let correlationId = referenceEvent.Metadata.CorrelationId\n\n                try\n                    // Add the event to the referenceEvents list, and save it to actor state.\n                    state.State.Add(referenceEvent)\n                    do! state.WriteStateAsync()\n\n                    // Update the referenceDto with the event.\n                    referenceDto <-\n                        referenceDto\n                        |> ReferenceDto.UpdateDto referenceEvent\n\n                    // Publish the event to the rest of the world.\n                    let graceEvent = GraceEvent.ReferenceEvent referenceEvent\n                    do! publishGraceEvent graceEvent referenceEvent.Metadata\n\n                    // If this is a Save or Checkpoint reference, schedule a physical deletion based on the default delays from the repository.\n                    match referenceEvent.Event with\n                    | Created (referenceId, ownerId, organizationId, repositoryId, branchId, directoryId, sha256Hash, referenceType, referenceText, links) ->\n                        do!\n                            match referenceDto.ReferenceType with\n                            | ReferenceType.Save ->\n                                task {\n                                    let repositoryActorProxy = Repository.CreateActorProxy referenceDto.OrganizationId referenceDto.RepositoryId correlationId\n                                    let! repositoryDto = repositoryActorProxy.Get correlationId\n\n                                    let reminderState: PhysicalDeletionReminderState =\n                                        {\n                                            RepositoryId = referenceDto.RepositoryId\n                                            BranchId = referenceDto.BranchId\n                                            DirectoryVersionId = referenceDto.DirectoryId\n                                            Sha256Hash = referenceDto.Sha256Hash\n                                            DeleteReason = $\"Save: automatic deletion after {repositoryDto.SaveDays} days\"\n                                            CorrelationId = correlationId\n                                        }\n\n                                    do!\n                                        (this :> IGraceReminderWithGuidKey)\n                                            .ScheduleReminderAsync\n                                            ReminderTypes.PhysicalDeletion\n                                            (Duration.FromDays(float repositoryDto.SaveDays))\n                                            (ReminderState.ReferencePhysicalDeletion reminderState)\n                                            correlationId\n                                }\n                            | ReferenceType.Checkpoint ->\n                                task {\n                                    let repositoryActorProxy = Repository.CreateActorProxy referenceDto.OrganizationId referenceDto.RepositoryId correlationId\n                                    let! repositoryDto = repositoryActorProxy.Get correlationId\n\n                                    let reminderState: PhysicalDeletionReminderState =\n                                        {\n                                            RepositoryId = referenceDto.RepositoryId\n                                            BranchId = referenceDto.BranchId\n                                            DirectoryVersionId = referenceDto.DirectoryId\n                                            Sha256Hash = referenceDto.Sha256Hash\n                                            DeleteReason = $\"Checkpoint: automatic deletion after {repositoryDto.CheckpointDays} days\"\n                                            CorrelationId = correlationId\n                                        }\n\n                                    do!\n                                        (this :> IGraceReminderWithGuidKey)\n                                            .ScheduleReminderAsync\n                                            ReminderTypes.PhysicalDeletion\n                                            (Duration.FromDays(float repositoryDto.CheckpointDays))\n                                            (ReminderState.ReferencePhysicalDeletion reminderState)\n                                            correlationId\n                                }\n                            | _ -> () |> returnTask\n                            :> Task\n                    | _ -> ()\n\n                    let graceReturnValue =\n                        (GraceReturnValue.Create referenceDto correlationId)\n                            .enhance(nameof RepositoryId, referenceDto.RepositoryId)\n                            .enhance(nameof BranchId, referenceDto.BranchId)\n                            .enhance(nameof ReferenceId, referenceDto.ReferenceId)\n                            .enhance(nameof DirectoryVersionId, referenceDto.DirectoryId)\n                            .enhance(nameof ReferenceType, getDiscriminatedUnionCaseName referenceDto.ReferenceType)\n                            .enhance (nameof ReferenceEventType, getDiscriminatedUnionFullName referenceEvent.Event)\n\n                    return Ok graceReturnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Failed to apply event {eventType} for reference {referenceId} in repository {repositoryId} on branch {branchId} with directory version {directoryVersionId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        getDiscriminatedUnionCaseName referenceEvent.Event,\n                        referenceDto.ReferenceId,\n                        referenceDto.RepositoryId,\n                        referenceDto.BranchId,\n                        referenceDto.DirectoryId\n                    )\n\n                    let graceError =\n                        (GraceError.CreateWithException ex (getErrorMessage ReferenceError.FailedWhileApplyingEvent) correlationId)\n                            .enhance(nameof RepositoryId, referenceDto.RepositoryId)\n                            .enhance(nameof BranchId, referenceDto.BranchId)\n                            .enhance(nameof ReferenceId, referenceDto.ReferenceId)\n                            .enhance(nameof DirectoryVersionId, referenceDto.DirectoryId)\n                            .enhance(nameof ReferenceType, getDiscriminatedUnionCaseName referenceDto.ReferenceType)\n                            .enhance (nameof ReferenceEventType, getDiscriminatedUnionFullName referenceEvent.Event)\n\n                    return Error graceError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = referenceDto.RepositoryId |> returnTask\n\n        interface IReferenceActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                not\n                <| referenceDto.ReferenceId.Equals(ReferenceDto.Default.ReferenceId)\n                |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                referenceDto |> returnTask\n\n            member this.GetReferenceType correlationId =\n                this.correlationId <- correlationId\n                referenceDto.ReferenceType |> returnTask\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                referenceDto.DeletedAt.IsSome |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (command: ReferenceCommand) (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (getErrorMessage ReferenceError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | Create (referenceId, ownerId, organizationId, repositoryId, branchId, directoryId, sha256Hash, referenceType, referenceText, links) ->\n                                match referenceDto.UpdatedAt with\n                                | Some _ -> return Error(GraceError.Create (getErrorMessage ReferenceError.ReferenceAlreadyExists) metadata.CorrelationId)\n                                | None -> return Ok command\n                            | _ ->\n                                match referenceDto.UpdatedAt with\n                                | Some _ -> return Ok command\n                                | None -> return Error(GraceError.Create (getErrorMessage ReferenceError.ReferenceIdDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let processCommand (command: ReferenceCommand) (metadata: EventMetadata) =\n                    task {\n                        let! referenceEventType =\n                            task {\n                                match command with\n                                | Create (referenceId,\n                                          ownerId,\n                                          organizationId,\n                                          repositoryId,\n                                          branchId,\n                                          directoryId,\n                                          sha256Hash,\n                                          referenceType,\n                                          referenceText,\n                                          links) ->\n                                    return\n                                        Created(\n                                            referenceId,\n                                            ownerId,\n                                            organizationId,\n                                            repositoryId,\n                                            branchId,\n                                            directoryId,\n                                            sha256Hash,\n                                            referenceType,\n                                            referenceText,\n                                            links\n                                        )\n                                | AddLink link -> return LinkAdded link\n                                | RemoveLink link -> return LinkRemoved link\n                                | DeleteLogical (force, deleteReason) ->\n                                    let tryGetLogicalDeleteDaysFromMetadata () =\n                                        match metadata.Properties.TryGetValue(\"RepositoryLogicalDeleteDays\") with\n                                        | true, value ->\n                                            let mutable parsed = 0.0f\n\n                                            if Single.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, &parsed) then\n                                                Some parsed\n                                            else\n                                                None\n                                        | _ -> None\n\n                                    let! logicalDeleteDays =\n                                        match tryGetLogicalDeleteDaysFromMetadata () with\n                                        | Some days -> Task.FromResult days\n                                        | None ->\n                                            task {\n                                                let repositoryActorProxy =\n                                                    Repository.CreateActorProxy referenceDto.OrganizationId referenceDto.RepositoryId this.correlationId\n\n                                                let! repositoryDto = repositoryActorProxy.Get this.correlationId\n                                                return repositoryDto.LogicalDeleteDays\n                                            }\n\n                                    let reminderState: PhysicalDeletionReminderState =\n                                        {\n                                            RepositoryId = referenceDto.RepositoryId\n                                            BranchId = referenceDto.BranchId\n                                            DirectoryVersionId = referenceDto.DirectoryId\n                                            Sha256Hash = referenceDto.Sha256Hash\n                                            DeleteReason = deleteReason\n                                            CorrelationId = metadata.CorrelationId\n                                        }\n\n                                    do!\n                                        (this :> IGraceReminderWithGuidKey)\n                                            .ScheduleReminderAsync\n                                            ReminderTypes.PhysicalDeletion\n                                            (Duration.FromDays(float logicalDeleteDays))\n                                            (ReminderState.ReferencePhysicalDeletion reminderState)\n                                            metadata.CorrelationId\n\n                                    return LogicalDeleted(force, deleteReason)\n                                | DeletePhysical ->\n                                    // Delete the actor state and mark the actor as deactivated.\n                                    do! state.ClearStateAsync()\n                                    this.DeactivateOnIdle()\n                                    return PhysicalDeleted\n                                | Undelete -> return Undeleted\n                            }\n\n                        let referenceEvent = { Event = referenceEventType; Metadata = metadata }\n                        let! returnValue = this.ApplyEvent referenceEvent\n\n                        return returnValue\n                    }\n\n                task {\n                    currentCommand <- $\"{getDiscriminatedUnionCaseName command} {getDiscriminatedUnionCaseName referenceDto.ReferenceType}\"\n                    this.correlationId <- metadata.CorrelationId\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Reminder.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Orleans\nopen Orleans.Runtime\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Concurrent\nopen System.Threading.Tasks\n\nmodule Reminder =\n\n    /// Orleans implementation of the ReminderActor.\n    type ReminderActor([<PersistentState(StateName.Reminder, Constants.GraceActorStorage)>] reminderState: IPersistentState<ReminderWrapper>) =\n        inherit Grain()\n\n        let log = loggerFactory.CreateLogger(\"Reminder.Actor\")\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage reminderState.RecordExists)\n\n            Task.CompletedTask\n\n        interface IReminderActor with\n            member this.Create (reminder: ReminderDto) (correlationId: CorrelationId) =\n                task {\n                    try\n                        reminderState.State.Reminder <- reminder\n                        do! reminderState.WriteStateAsync()\n\n                        log.LogTrace(\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Created reminder {ReminderId}. Actor {ActorName}||{ActorId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            reminder.CorrelationId,\n                            reminder.ReminderId,\n                            reminder.ActorName,\n                            reminder.ActorId\n                        )\n\n                        return ()\n                    with\n                    | ex ->\n                        log.LogError(\n                            ex,\n                            \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Error creating reminder {ReminderId}. Actor {ActorName}||{ActorId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            correlationId,\n                            reminder.ReminderId,\n                            reminder.ActorName,\n                            reminder.ActorId\n                        )\n\n                        return ()\n                }\n                :> Task\n\n            member this.Delete(correlationId: CorrelationId) =\n                task {\n                    let reminderDto = reminderState.State.Reminder\n\n                    try\n                        this.correlationId <- correlationId\n                        do! reminderState.ClearStateAsync()\n\n                        if not reminderState.RecordExists then\n                            log.LogInformation(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Deleted reminder {ReminderId}. Actor {ActorName}||{ActorId}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                correlationId,\n                                reminderDto.ReminderId,\n                                reminderDto.ActorName,\n                                reminderDto.ActorId\n                            )\n                        else\n                            log.LogWarning(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; State for Reminder {ReminderId} was not deleted. It may not have been found. Actor {ActorName}||{ActorId}.\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                correlationId,\n                                reminderDto.ReminderId,\n                                reminderDto.ActorName,\n                                reminderDto.ActorId\n                            )\n\n                        return ()\n                    with\n                    | ex ->\n                        log.LogError(\n                            \"{CurrentInstant}:  Node: {HostName}; CorrelationId: {CorrelationId}; Error deleting reminder {ReminderId}. Actor {ActorName}||{ActorId}. {ExceptionDetails}\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            correlationId,\n                            reminderDto.ReminderId,\n                            reminderDto.ActorName,\n                            reminderDto.ActorId,\n                            ExceptionResponse.Create ex\n                        )\n\n                        return ()\n                }\n                :> Task\n\n            member this.Exists(correlationId: CorrelationId) : Task<bool> =\n                this.correlationId <- correlationId\n\n                if reminderState.State.Reminder.ReminderTime = Instant.MinValue then\n                    false |> returnTask\n                else\n                    true |> returnTask\n\n            member this.Get(correlationId: CorrelationId) =\n                this.correlationId <- correlationId\n                reminderState.State.Reminder |> returnTask\n\n            member this.Remind(correlationId: CorrelationId) : Task<Result<unit, GraceError>> =\n                task {\n                    let reminderDto = reminderState.State.Reminder\n\n                    try\n                        this.correlationId <- correlationId\n\n                        // Parse the Guid from the ActorId. Example: \"referenceactor/da3926330c394275813d95e390a5c374\"\n                        let actorId =\n                            if reminderDto.ActorName = ActorName.Diff then\n                                // Diff actors have a different ActorId format: \"directoryVersionId1*directoryVersionId2\"\n                                Guid.Empty\n                            else\n                                Guid.ParseExact(reminderDto.ActorId.Split(\"/\").[1], \"N\")\n\n                        match reminderDto.ActorName with\n                        | ActorName.Owner ->\n                            let ownerActorProxy = Owner.CreateActorProxy actorId correlationId\n                            return! ownerActorProxy.ReceiveReminderAsync reminderDto\n                        | ActorName.Organization ->\n                            let organizationActorProxy = Organization.CreateActorProxy actorId correlationId\n                            return! organizationActorProxy.ReceiveReminderAsync reminderDto\n                        | ActorName.Repository ->\n                            let repositoryActorProxy = Repository.CreateActorProxy actorId reminderDto.RepositoryId correlationId\n                            return! repositoryActorProxy.ReceiveReminderAsync reminderDto\n                        | ActorName.Branch ->\n                            let branchActorProxy = Branch.CreateActorProxy actorId reminderDto.RepositoryId correlationId\n                            return! branchActorProxy.ReceiveReminderAsync reminderDto\n                        | ActorName.DirectoryVersion ->\n                            let directoryVersionActorProxy = DirectoryVersion.CreateActorProxy actorId reminderDto.RepositoryId correlationId\n\n                            return! directoryVersionActorProxy.ReceiveReminderAsync reminderDto\n                        | ActorName.Reference ->\n                            let referenceActorProxy = Reference.CreateActorProxy actorId reminderDto.RepositoryId correlationId\n                            return! referenceActorProxy.ReceiveReminderAsync reminderDto\n                        | ActorName.Diff ->\n                            // Example reminderDto.ActorId: \"diffactor/15b50c95-7306-4ecb-9850-a0a5dc7419cf*1e7b6f83-4715-42f8-ba0b-9b0262356f08\"\n                            let directoryVersionIds = reminderDto.ActorId.Split(\"/\").[1].Split(\"*\")\n\n                            let diffActorProxy =\n                                Diff.CreateActorProxy\n                                    (Guid.ParseExact(directoryVersionIds[0], \"D\")) // \"D\" = 32 digits separated by hyphens\n                                    (Guid.ParseExact(directoryVersionIds[1], \"D\"))\n                                    reminderDto.OwnerId\n                                    reminderDto.OrganizationId\n                                    reminderDto.RepositoryId\n                                    correlationId\n\n                            return! diffActorProxy.ReceiveReminderAsync reminderDto\n                        | _ -> return Ok()\n                    with\n                    | ex ->\n                        log.LogError(\n                            \"{CurrentInstant}:  Node: {HostName}; CorrelationId: {CorrelationId}; Error reminding actor {ActorName}||{ActorId}. {ExceptionDetails}\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            correlationId,\n                            reminderDto.ActorName,\n                            reminderDto.ActorId,\n                            ExceptionResponse.Create ex\n                        )\n\n                        return\n                            Error(\n                                (GraceError.Create \"Failed to execute reminder.\" correlationId)\n                                    .enhance(\"reminder\", (serialize reminderDto))\n                                    .enhance (\"exception\", $\"{ExceptionResponse.Create ex}\")\n                            )\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Repository.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen FSharp.Control\nopen FSharpPlus\nopen Grace.Actors.Constants\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Combinators\nopen Grace.Shared.Constants\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Resources.Utilities\nopen Grace.Types.Branch\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.Events\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Globalization\nopen System.Linq\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\nopen System.Runtime.Serialization\nopen Grace.Shared.Services\n\nmodule Repository =\n\n    type RepositoryActor([<PersistentState(StateName.Repository, Constants.GraceActorStorage)>] state: IPersistentState<List<RepositoryEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Repository\n\n        let log = loggerFactory.CreateLogger(\"Repository.Actor\")\n\n        let mutable repositoryDto = RepositoryDto.Default\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            repositoryDto <-\n                state.State\n                |> Seq.fold\n                    (fun repositoryDto repositoryEvent ->\n                        repositoryDto\n                        |> RepositoryDto.UpdateDto repositoryEvent)\n                    RepositoryDto.Default\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent repositoryEvent =\n            task {\n                try\n                    // Add the new event to the list of events, and write the state to storage.\n                    state.State.Add repositoryEvent\n                    do! state.WriteStateAsync()\n\n                    // Update the repositoryDto with the new event.\n                    repositoryDto <-\n                        repositoryDto\n                        |> RepositoryDto.UpdateDto repositoryEvent\n\n                    /// Concatenates repository errors into a single GraceError instance.\n                    let processGraceError (repositoryError: RepositoryError) repositoryEvent previousGraceError =\n                        Error(\n                            GraceError.Create\n                                $\"{getErrorMessage repositoryError}{Environment.NewLine}{previousGraceError.Error}\"\n                                repositoryEvent.Metadata.CorrelationId\n                        )\n\n                    // If we're creating a repository, we need to create the default branch, the initial promotion, and the initial directory.\n                    //   Otherwise, just pass the event through.\n                    let handleEvent =\n                        task {\n                            match repositoryEvent.Event with\n                            | Created (name, repositoryId, ownerId, organizationId, objectStorageProvider) ->\n                                // Create the default branch.\n                                let branchId = (Guid.NewGuid())\n                                let branchActor = Branch.CreateActorProxy branchId repositoryDto.RepositoryId this.correlationId\n\n                                // Only allow promotions and tags on the initial branch.\n                                let initialBranchPermissions =\n                                    [|\n                                        ReferenceType.Promotion\n                                        ReferenceType.Tag\n                                        ReferenceType.External\n                                    |]\n\n                                let createInitialBranchCommand =\n                                    BranchCommand.Create(\n                                        branchId,\n                                        InitialBranchName,\n                                        DefaultParentBranchId,\n                                        ReferenceId.Empty,\n                                        ownerId,\n                                        organizationId,\n                                        repositoryId,\n                                        initialBranchPermissions\n                                    )\n\n                                match! branchActor.Handle createInitialBranchCommand repositoryEvent.Metadata with\n                                | Ok branchGraceReturn ->\n                                    logToConsole $\"In Repository.Actor.handleEvent: Successfully created the new branch.\"\n                                    // Create an empty directory version, and use that for the initial promotion\n                                    let emptyDirectoryId = DirectoryVersionId.NewGuid()\n\n                                    let emptySha256Hash = computeSha256ForDirectory RootDirectoryPath (List<LocalDirectoryVersion>()) (List<LocalFileVersion>())\n\n                                    let directoryVersionActorProxy =\n                                        DirectoryVersion.CreateActorProxy emptyDirectoryId repositoryDto.RepositoryId this.correlationId\n\n                                    let emptyDirectoryVersion =\n                                        DirectoryVersion.Create\n                                            emptyDirectoryId\n                                            repositoryDto.OwnerId\n                                            repositoryDto.OrganizationId\n                                            repositoryDto.RepositoryId\n                                            RootDirectoryPath\n                                            emptySha256Hash\n                                            (List<DirectoryVersionId>())\n                                            (List<FileVersion>())\n                                            0L\n\n                                    let! directoryResult =\n                                        directoryVersionActorProxy.Handle\n                                            (DirectoryVersionCommand.Create(emptyDirectoryVersion, repositoryDto))\n                                            repositoryEvent.Metadata\n\n                                    logToConsole $\"In Repository.Actor.handleEvent: Successfully created the empty directory version.\"\n\n                                    let! promotionResult =\n                                        branchActor.Handle\n                                            (BranchCommand.Promote(\n                                                emptyDirectoryId,\n                                                emptySha256Hash,\n                                                (getLocalizedString StringResourceName.InitialPromotionMessage)\n                                            ))\n                                            repositoryEvent.Metadata\n\n                                    logToConsole $\"In Repository.Actor.handleEvent: After trying to create the first promotion.\"\n\n                                    match directoryResult, promotionResult with\n                                    | (Ok directoryVersionGraceReturnValue, Ok promotionGraceReturnValue) ->\n                                        logToConsole $\"In Repository.Actor.handleEvent: Successfully created the initial promotion.\"\n\n                                        //logToConsole $\"promotionGraceReturnValue.Properties:\"\n\n                                        //promotionGraceReturnValue.Properties\n                                        //|> Seq.iter (fun kv -> logToConsole $\"  {kv.Key}: {kv.Value}\")\n                                        // Set current, empty directory as the based-on reference.\n                                        let referenceId = Guid.Parse($\"{promotionGraceReturnValue.Properties[nameof ReferenceId]}\")\n\n                                        //logToConsole $\"In Repository.Actor.handleEvent: Before trying to rebase the initial branch.\"\n                                        //let! rebaseResult = branchActor.Handle (Commands.Branch.BranchCommand.Rebase(referenceId)) repositoryEvent.Metadata\n                                        //logToConsole $\"In Repository.Actor.handleEvent: After trying to rebase the initial branch.\"\n\n\n                                        //match rebaseResult with\n                                        //| Ok rebaseGraceReturn -> return Ok(branchId, referenceId)\n                                        //| Error graceError -> return processGraceError FailedRebasingInitialBranch repositoryEvent graceError\n                                        return Ok(branchId, referenceId)\n                                    | (_, Error graceError) -> return processGraceError FailedCreatingInitialPromotion repositoryEvent graceError\n                                    | (Error graceError, _) -> return processGraceError FailedCreatingEmptyDirectoryVersion repositoryEvent graceError\n                                | Error graceError ->\n                                    logToConsole $\"In Repository.Actor.handleEvent: Failed to create the new branch.\"\n                                    return processGraceError FailedCreatingInitialBranch repositoryEvent graceError\n                            | _ -> return Ok(BranchId.Empty, ReferenceId.Empty)\n                        }\n\n                    match! handleEvent with\n                    | Ok (branchId, referenceId) ->\n                        // Publish the event to the rest of the world.\n                        let graceEvent = GraceEvent.RepositoryEvent repositoryEvent\n                        do! publishGraceEvent graceEvent repositoryEvent.Metadata\n\n                        let returnValue = GraceReturnValue.Create $\"Repository command succeeded.\" repositoryEvent.Metadata.CorrelationId\n\n                        returnValue\n                            .enhance(nameof OwnerId, repositoryDto.OwnerId)\n                            .enhance(nameof OrganizationId, repositoryDto.OrganizationId)\n                            .enhance(nameof RepositoryId, repositoryDto.RepositoryId)\n                            .enhance(nameof RepositoryName, repositoryDto.RepositoryName)\n                            .enhance (nameof RepositoryEventType, getDiscriminatedUnionFullName repositoryEvent.Event)\n                        |> ignore\n\n                        if branchId <> BranchId.Empty then\n                            returnValue\n                                .enhance(nameof BranchId, branchId)\n                                .enhance(nameof BranchName, Constants.InitialBranchName)\n                                .enhance (nameof ReferenceId, referenceId)\n                            |> ignore\n\n                        returnValue.Properties.Add(\"EventType\", getDiscriminatedUnionFullName repositoryEvent.Event)\n\n                        return Ok returnValue\n                    | Error graceError -> return Error graceError\n                with\n                | ex ->\n                    let exceptionResponse = ExceptionResponse.Create ex\n\n                    let graceError = GraceError.Create (getErrorMessage RepositoryError.FailedWhileApplyingEvent) repositoryEvent.Metadata.CorrelationId\n\n                    graceError\n                        .enhance(\n                            \"Exception details\",\n                            exceptionResponse.``exception``\n                            + exceptionResponse.innerException\n                        )\n                        .enhance(nameof OwnerId, repositoryDto.OwnerId)\n                        .enhance(nameof OrganizationId, repositoryDto.OrganizationId)\n                        .enhance(nameof RepositoryId, repositoryDto.RepositoryId)\n                        .enhance(nameof RepositoryName, repositoryDto.RepositoryName)\n                        .enhance (nameof RepositoryEventType, getDiscriminatedUnionFullName repositoryEvent.Event)\n                    |> ignore\n\n                    return Error graceError\n            }\n\n        /// Deletes all of the branches provided, by sending a DeleteLogical command to each branch.\n        member private this.LogicalDeleteBranches(branches: BranchDto array, metadata: EventMetadata, deleteReason: DeleteReason) =\n            task {\n                let results = ConcurrentQueue<GraceResult<string>>()\n\n                // Loop through each branch and send a DeleteLogical command to it.\n                do!\n                    Parallel.ForEachAsync(\n                        branches,\n                        Constants.ParallelOptions,\n                        (fun branch ct ->\n                            ValueTask(\n                                task {\n                                    if branch.DeletedAt |> Option.isNone then\n                                        let branchActor = Branch.CreateActorProxy branch.BranchId branch.RepositoryId this.correlationId\n\n                                        let childMetadata = EventMetadata.New metadata.CorrelationId GraceSystemUser\n                                        childMetadata.Properties[ nameof RepositoryId ] <- $\"{repositoryDto.RepositoryId}\"\n\n                                        childMetadata.Properties[ \"RepositoryLogicalDeleteDays\" ] <-\n                                            repositoryDto.LogicalDeleteDays.ToString(\"F\", CultureInfo.InvariantCulture)\n\n                                        let! result =\n                                            branchActor.Handle\n                                                (BranchCommand.DeleteLogical(\n                                                    true,\n                                                    $\"Cascaded from deleting repository. ownerId: {repositoryDto.OwnerId}; organizationId: {repositoryDto.OrganizationId}; repositoryId: {repositoryDto.RepositoryId}; repositoryName: {repositoryDto.RepositoryName}; deleteReason: {deleteReason}\",\n                                                    false,\n                                                    None\n                                                ))\n                                                childMetadata\n\n                                        results.Enqueue(result)\n                                }\n                            ))\n                    )\n\n                // Check if any of the results were errors, and take the first one if so.\n                let overallResult =\n                    results\n                    |> Seq.tryPick (fun result ->\n                        match result with\n                        | Ok _ -> None\n                        | Error error -> Some(error))\n\n                match overallResult with\n                | None -> return Ok()\n                | Some error -> return Error error\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = repositoryDto.RepositoryId |> returnTask\n\n        interface IGraceReminderWithGuidKey with\n            /// Schedules a Grace reminder.\n            member this.ScheduleReminderAsync reminderType delay state correlationId =\n                task {\n                    let reminder =\n                        ReminderDto.Create\n                            actorName\n                            $\"{this.IdentityString}\"\n                            repositoryDto.OwnerId\n                            repositoryDto.OrganizationId\n                            repositoryDto.RepositoryId\n                            reminderType\n                            (getFutureInstant delay)\n                            state\n                            correlationId\n\n                    do! createReminder reminder\n                }\n                :> Task\n\n            /// Receives a Grace reminder.\n            member this.ReceiveReminderAsync(reminder: ReminderDto) : Task<Result<unit, GraceError>> =\n                task {\n                    match reminder.ReminderType, reminder.State with\n                    | ReminderTypes.PhysicalDeletion, ReminderState.RepositoryPhysicalDeletion physicalDeletionReminderState ->\n                        this.correlationId <- physicalDeletionReminderState.CorrelationId\n\n                        do! state.ClearStateAsync()\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Deleted physical state for repository; RepositoryId: {}; RepositoryName: {}; OrganizationId: {organizationId}; OwnerId: {ownerId}; deleteReason: {deleteReason}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            physicalDeletionReminderState.CorrelationId,\n                            repositoryDto.RepositoryId,\n                            repositoryDto.RepositoryName,\n                            repositoryDto.OrganizationId,\n                            repositoryDto.OwnerId,\n                            physicalDeletionReminderState.DeleteReason\n                        )\n\n                        this.DeactivateOnIdle()\n                        return Ok()\n                    | reminderType, state ->\n                        return\n                            Error(\n                                GraceError.Create\n                                    $\"{actorName} does not process reminder type {getDiscriminatedUnionCaseName reminderType} with state {getDiscriminatedUnionCaseName state}.\"\n                                    this.correlationId\n                            )\n                }\n\n        interface IExportable<RepositoryEvent> with\n            member this.Export() =\n                task {\n                    try\n                        if state.State.Count > 0 then\n                            return Ok state.State\n                        else\n                            return Error ExportError.EventListIsEmpty\n                    with\n                    | ex -> return Error(ExportError.Exception(ExceptionResponse.Create ex))\n                }\n\n            member this.Import(events: IReadOnlyList<RepositoryEvent>) =\n                task {\n                    try\n                        state.State.Clear()\n                        state.State.AddRange(events)\n                        do! state.WriteStateAsync()\n                        return Ok events.Count\n                    with\n                    | ex -> return Error(ImportError.Exception(ExceptionResponse.Create ex))\n                }\n\n        interface IRevertable<RepositoryDto> with\n            member this.RevertBack (eventsToRevert: int) (persist: PersistAction) =\n                task {\n                    try\n                        let repositoryEvents = state.State\n\n                        if repositoryEvents.Count > 0 then\n                            let eventsToKeep = repositoryEvents.Count - eventsToRevert\n\n                            if eventsToKeep <= 0 then\n                                return Error RevertError.OutOfRange\n                            else\n                                let revertedEvents = repositoryEvents.Take eventsToKeep\n\n                                let newRepositoryDto = revertedEvents.Aggregate(RepositoryDto.Default, (fun state evnt -> (RepositoryDto.UpdateDto evnt state)))\n\n                                match persist with\n                                | PersistAction.Save ->\n                                    state.State.Clear()\n                                    state.State.AddRange revertedEvents\n                                    do! state.WriteStateAsync()\n                                | DoNotSave -> ()\n\n                                return Ok newRepositoryDto\n                        else\n                            return Error RevertError.EmptyEventList\n                    with\n                    | ex -> return Error(RevertError.Exception(ExceptionResponse.Create ex))\n                }\n\n            member this.RevertToInstant (whenToRevertTo: Instant) (persist: PersistAction) =\n                task {\n                    try\n                        let repositoryEvents = state.State\n\n                        if repositoryEvents.Count > 0 then\n                            let revertedEvents = repositoryEvents.Where(fun evnt -> evnt.Metadata.Timestamp < whenToRevertTo)\n\n                            if revertedEvents.Count() = 0 then\n                                return Error RevertError.OutOfRange\n                            else\n                                let newRepositoryDto =\n                                    revertedEvents\n                                    |> Seq.fold (fun state evnt -> (RepositoryDto.UpdateDto evnt state)) RepositoryDto.Default\n\n                                match persist with\n                                | PersistAction.Save ->\n                                    task {\n                                        state.State.Clear()\n                                        state.State.AddRange revertedEvents\n                                        do! state.WriteStateAsync()\n                                    }\n                                    |> ignore\n                                | DoNotSave -> ()\n\n                                return Ok newRepositoryDto\n                        else\n                            return Error RevertError.EmptyEventList\n                    with\n                    | ex -> return Error(RevertError.Exception(ExceptionResponse.Create ex))\n                }\n\n            member this.EventCount() = task { return state.State.Count }\n\n        interface IRepositoryActor with\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                repositoryDto |> returnTask\n\n            member this.GetObjectStorageProvider correlationId =\n                this.correlationId <- correlationId\n                repositoryDto.ObjectStorageProvider |> returnTask\n\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n                repositoryDto.UpdatedAt.IsSome |> returnTask\n\n            member this.IsEmpty correlationId =\n                this.correlationId <- correlationId\n                repositoryDto.InitializedAt.IsNone |> returnTask\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                repositoryDto.DeletedAt.IsSome |> returnTask\n\n            member this.Handle command metadata =\n                let isValid command (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (getErrorMessage RepositoryError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | RepositoryCommand.Create (_, _, _, _, _) ->\n                                match repositoryDto.UpdatedAt with\n                                | Some _ -> return Error(GraceError.Create (getErrorMessage RepositoryError.RepositoryIdAlreadyExists) metadata.CorrelationId)\n                                | None -> return Ok command\n                            | _ ->\n                                match repositoryDto.UpdatedAt with\n                                | Some _ -> return Ok command\n                                | None -> return Error(GraceError.Create (getErrorMessage RepositoryError.RepositoryIdDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let processCommand command (metadata: EventMetadata) =\n                    task {\n                        try\n                            let! event =\n                                task {\n                                    match command with\n                                    | Create (repositoryName, repositoryId, ownerId, organizationId, objectStorageProvider) ->\n                                        return Created(repositoryName, repositoryId, ownerId, organizationId, objectStorageProvider)\n                                    | Initialize -> return Initialized\n                                    | SetObjectStorageProvider objectStorageProvider -> return ObjectStorageProviderSet objectStorageProvider\n                                    | SetStorageAccountName storageAccountName -> return StorageAccountNameSet storageAccountName\n                                    | SetStorageContainerName containerName -> return StorageContainerNameSet containerName\n                                    | SetRepositoryStatus repositoryStatus -> return RepositoryStatusSet repositoryStatus\n                                    | SetRepositoryType repositoryType -> return RepositoryTypeSet repositoryType\n                                    | SetAllowsLargeFiles allowsLargeFiles -> return AllowsLargeFilesSet allowsLargeFiles\n                                    | SetAnonymousAccess anonymousAccess -> return AnonymousAccessSet anonymousAccess\n                                    | SetRecordSaves recordSaves -> return RecordSavesSet recordSaves\n                                    | SetDefaultServerApiVersion version -> return DefaultServerApiVersionSet version\n                                    | SetDefaultBranchName defaultBranchName -> return DefaultBranchNameSet defaultBranchName\n                                    | SetLogicalDeleteDays days -> return LogicalDeleteDaysSet days\n                                    | SetSaveDays days -> return SaveDaysSet days\n                                    | SetCheckpointDays days -> return CheckpointDaysSet days\n                                    | SetDirectoryVersionCacheDays days -> return DirectoryVersionCacheDaysSet days\n                                    | SetDiffCacheDays days -> return DiffCacheDaysSet days\n                                    | SetName repositoryName -> return NameSet repositoryName\n                                    | SetDescription description -> return DescriptionSet description\n                                    | SetConflictResolutionPolicy policy -> return ConflictResolutionPolicySet policy\n                                    | DeleteLogical (force, deleteReason) ->\n                                        // Get the list of branches that aren't already deleted.\n                                        let! branches =\n                                            getBranches\n                                                repositoryDto.OwnerId\n                                                repositoryDto.OrganizationId\n                                                repositoryDto.RepositoryId\n                                                Int32.MaxValue\n                                                false\n                                                metadata.CorrelationId\n\n                                        // If any branches are not already deleted, and we're not forcing the deletion, then throw an exception.\n                                        if not <| force\n                                           && branches.Length > 0\n                                           && branches.Any(fun branch -> branch.DeletedAt |> Option.isNone) then\n                                            return LogicalDeleted(force, deleteReason)\n                                        else\n                                            // We have --force specified, so delete the branches that aren't already deleted.\n                                            match! this.LogicalDeleteBranches(branches, metadata, deleteReason) with\n                                            | Ok _ ->\n                                                let physicalDeletionReminderState = { DeleteReason = deleteReason; CorrelationId = metadata.CorrelationId }\n\n                                                do!\n                                                    (this :> IGraceReminderWithGuidKey)\n                                                        .ScheduleReminderAsync\n                                                        ReminderTypes.PhysicalDeletion\n                                                        (Duration.FromDays(float repositoryDto.LogicalDeleteDays))\n                                                        (ReminderState.RepositoryPhysicalDeletion physicalDeletionReminderState)\n                                                        metadata.CorrelationId\n\n                                                ()\n                                            | Error error -> raise (ApplicationException($\"{error}\"))\n\n                                            return LogicalDeleted(force, deleteReason)\n                                    | DeletePhysical ->\n                                        // Delete the state from storage, and deactivate the actor.\n                                        do! state.ClearStateAsync()\n                                        this.DeactivateOnIdle()\n                                        return PhysicalDeleted\n                                    | RepositoryCommand.Undelete -> return Undeleted\n                                }\n\n                            return! this.ApplyEvent { Event = event; Metadata = metadata }\n                        with\n                        | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}{Environment.NewLine}{metadata}\" metadata.CorrelationId)\n                    }\n\n                task {\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Repository.Actor.fs (ApplyEvent Method)",
    "content": ""
  },
  {
    "path": "src/Grace.Actors/RepositoryName.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule RepositoryName =\n\n    type RepositoryNameActor() =\n        inherit Grain()\n\n        static let actorName = ActorName.RepositoryName\n\n        let log = loggerFactory.CreateLogger(\"RepositoryName.Actor\")\n\n        let mutable cachedRepositoryId: RepositoryId option = None\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime \"In-memory only\"\n\n            Task.CompletedTask\n\n        interface IRepositoryNameActor with\n            member this.GetRepositoryId correlationId =\n                this.correlationId <- correlationId\n                cachedRepositoryId |> returnTask\n\n            member this.SetRepositoryId (repositoryId: RepositoryId) correlationId =\n                this.correlationId <- correlationId\n                cachedRepositoryId <- Some repositoryId\n\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/RepositoryPermission.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule RepositoryPermission =\n\n    let ActorName = ActorName.RepositoryPermission\n\n    [<GenerateSerializer>]\n    type RepositoryPermissionState = { PathPermissions: PathPermission list }\n\n    module RepositoryPermissionState =\n        let Empty = { PathPermissions = [] }\n\n    type RepositoryPermissionActor\n        (\n            [<PersistentState(StateName.RepositoryPermission, Grace.Shared.Constants.GraceActorStorage)>] state: IPersistentState<RepositoryPermissionState>\n        ) =\n        inherit Grain()\n\n        let log = loggerFactory.CreateLogger(\"RepositoryPermission.Actor\")\n\n        let mutable permissionState = RepositoryPermissionState.Empty\n\n        override this.OnActivateAsync(ct) =\n            permissionState <- if state.RecordExists then state.State else RepositoryPermissionState.Empty\n\n            Task.CompletedTask\n\n        member private this.SaveState() =\n            task {\n                state.State <- permissionState\n\n                if permissionState.PathPermissions |> List.isEmpty then\n                    do! DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> state.ClearStateAsync())\n                else\n                    do! DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> state.WriteStateAsync())\n            }\n\n        member private this.Upsert (pathPermission: PathPermission) (metadata: EventMetadata) =\n            task {\n                let normalizedPath = normalizeFilePath pathPermission.Path\n                let normalizedPermission = { pathPermission with Path = normalizedPath }\n\n                let updated =\n                    permissionState.PathPermissions\n                    |> List.filter (fun existing -> normalizeFilePath existing.Path <> normalizedPath)\n\n                permissionState <- { permissionState with PathPermissions = normalizedPermission :: updated }\n                do! this.SaveState()\n\n                let returnValue = GraceReturnValue.Create permissionState.PathPermissions metadata.CorrelationId\n                return Ok returnValue\n            }\n\n        member private this.Remove (path: RelativePath) (metadata: EventMetadata) =\n            task {\n                let normalizedPath = normalizeFilePath path\n\n                let updated =\n                    permissionState.PathPermissions\n                    |> List.filter (fun existing -> normalizeFilePath existing.Path <> normalizedPath)\n\n                permissionState <- { permissionState with PathPermissions = updated }\n                do! this.SaveState()\n\n                let returnValue = GraceReturnValue.Create permissionState.PathPermissions metadata.CorrelationId\n                return Ok returnValue\n            }\n\n        member private this.List (pathFilter: RelativePath option) (metadata: EventMetadata) =\n            task {\n                let filtered =\n                    match pathFilter with\n                    | None -> permissionState.PathPermissions\n                    | Some value ->\n                        let normalizedPath = normalizeFilePath value\n\n                        permissionState.PathPermissions\n                        |> List.filter (fun existing -> normalizeFilePath existing.Path = normalizedPath)\n\n                let returnValue = GraceReturnValue.Create filtered metadata.CorrelationId\n                return Ok returnValue\n            }\n\n        interface IRepositoryPermissionActor with\n            member this.Handle command metadata =\n                match command with\n                | RepositoryPermissionCommand.UpsertPathPermission pathPermission -> this.Upsert pathPermission metadata\n                | RepositoryPermissionCommand.RemovePathPermission path -> this.Remove path metadata\n                | RepositoryPermissionCommand.ListPathPermissions path -> this.List path metadata\n\n            member this.GetPathPermissions pathFilter correlationId =\n                let filtered =\n                    match pathFilter with\n                    | None -> permissionState.PathPermissions\n                    | Some value ->\n                        let normalizedPath = normalizeFilePath value\n\n                        permissionState.PathPermissions\n                        |> List.filter (fun existing -> normalizeFilePath existing.Path = normalizedPath)\n\n                filtered |> returnTask\n"
  },
  {
    "path": "src/Grace.Actors/Review.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Events\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule Review =\n\n    type ReviewActor([<PersistentState(StateName.Review, Constants.GraceActorStorage)>] state: IPersistentState<List<ReviewEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.Review\n\n        let log = loggerFactory.CreateLogger(\"Review.Actor\")\n\n        let mutable currentCommand = String.Empty\n\n        let mutable reviewNotes: ReviewNotes option = None\n\n        let mutable checkpoints: ReviewCheckpoint list = []\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            let applyToState (reviewEvent: ReviewEvent) =\n                match reviewEvent.Event with\n                | NotesUpserted notes ->\n                    let createdAt =\n                        if notes.CreatedAt = Constants.DefaultTimestamp then\n                            reviewEvent.Metadata.Timestamp\n                        else\n                            notes.CreatedAt\n\n                    let updatedNotes = { notes with CreatedAt = createdAt; UpdatedAt = Some reviewEvent.Metadata.Timestamp }\n\n                    reviewNotes <- Some updatedNotes\n                | FindingResolved (findingId, resolutionState, resolvedBy, note) ->\n                    reviewNotes <-\n                        reviewNotes\n                        |> Option.map (fun notes ->\n                            let updatedFindings =\n                                notes.Findings\n                                |> List.map (fun finding ->\n                                    if finding.FindingId = findingId then\n                                        { finding with\n                                            ResolutionState = resolutionState\n                                            ResolvedBy = Some resolvedBy\n                                            ResolvedAt = Some reviewEvent.Metadata.Timestamp\n                                            ResolutionNote = note\n                                        }\n                                    else\n                                        finding)\n\n                            { notes with Findings = updatedFindings; UpdatedAt = Some reviewEvent.Metadata.Timestamp })\n                | CheckpointAdded checkpoint -> checkpoints <- checkpoints @ [ checkpoint ]\n\n            state.State |> Seq.iter applyToState\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent(reviewEvent: ReviewEvent) =\n            task {\n                let correlationId = reviewEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(reviewEvent)\n                    do! state.WriteStateAsync()\n\n                    match reviewEvent.Event with\n                    | NotesUpserted notes ->\n                        let createdAt =\n                            if notes.CreatedAt = Constants.DefaultTimestamp then\n                                reviewEvent.Metadata.Timestamp\n                            else\n                                notes.CreatedAt\n\n                        let updatedNotes = { notes with CreatedAt = createdAt; UpdatedAt = Some reviewEvent.Metadata.Timestamp }\n\n                        reviewNotes <- Some updatedNotes\n                    | FindingResolved (findingId, resolutionState, resolvedBy, note) ->\n                        reviewNotes <-\n                            reviewNotes\n                            |> Option.map (fun notes ->\n                                let updatedFindings =\n                                    notes.Findings\n                                    |> List.map (fun finding ->\n                                        if finding.FindingId = findingId then\n                                            { finding with\n                                                ResolutionState = resolutionState\n                                                ResolvedBy = Some resolvedBy\n                                                ResolvedAt = Some reviewEvent.Metadata.Timestamp\n                                                ResolutionNote = note\n                                            }\n                                        else\n                                            finding)\n\n                                { notes with Findings = updatedFindings; UpdatedAt = Some reviewEvent.Metadata.Timestamp })\n                    | CheckpointAdded checkpoint -> checkpoints <- checkpoints @ [ checkpoint ]\n\n                    let graceEvent = GraceEvent.ReviewEvent reviewEvent\n                    do! publishGraceEvent graceEvent reviewEvent.Metadata\n\n                    let returnValue =\n                        (GraceReturnValue.Create \"Review command succeeded.\" correlationId)\n                            .enhance(\n                                nameof ReviewNotesId,\n                                reviewNotes\n                                |> Option.map (fun notes -> notes.ReviewNotesId)\n                                |> Option.defaultValue (ReviewNotesId.Empty)\n                            )\n                            .enhance (nameof ReviewEventType, getDiscriminatedUnionFullName reviewEvent.Event)\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Failed to apply event {eventType} for review.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        getDiscriminatedUnionCaseName reviewEvent.Event\n                    )\n\n                    let graceError =\n                        (GraceError.CreateWithException ex (ReviewError.getErrorMessage ReviewError.FailedWhileApplyingEvent) correlationId)\n                            .enhance (\n                                nameof ReviewNotesId,\n                                reviewNotes\n                                |> Option.map (fun notes -> notes.ReviewNotesId)\n                                |> Option.defaultValue (ReviewNotesId.Empty)\n                            )\n\n                    return Error graceError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId =\n                let repositoryId =\n                    reviewNotes\n                    |> Option.map (fun notes -> notes.RepositoryId)\n                    |> Option.defaultValue RepositoryId.Empty\n\n                repositoryId |> returnTask\n\n        interface IReviewActor with\n            member this.GetNotes correlationId =\n                this.correlationId <- correlationId\n                reviewNotes |> returnTask\n\n            member this.GetCheckpoints correlationId =\n                this.correlationId <- correlationId\n\n                (checkpoints :> IReadOnlyList<ReviewCheckpoint>)\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (command: ReviewCommand) (metadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId) then\n                            return Error(GraceError.Create (ReviewError.getErrorMessage ReviewError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | UpsertNotes _ -> return Ok command\n                            | AddCheckpoint _ -> return Ok command\n                            | ResolveFinding (findingId, _, _, _) ->\n                                match reviewNotes with\n                                | None ->\n                                    return Error(GraceError.Create (ReviewError.getErrorMessage ReviewError.ReviewNotesDoesNotExist) metadata.CorrelationId)\n                                | Some notes ->\n                                    let exists =\n                                        notes.Findings\n                                        |> List.exists (fun finding -> finding.FindingId = findingId)\n\n                                    if exists then\n                                        return Ok command\n                                    else\n                                        return Error(GraceError.Create (ReviewError.getErrorMessage ReviewError.FindingDoesNotExist) metadata.CorrelationId)\n                    }\n\n                let processCommand (command: ReviewCommand) (metadata: EventMetadata) =\n                    task {\n                        let! reviewEventType =\n                            task {\n                                match command with\n                                | UpsertNotes notes -> return NotesUpserted notes\n                                | ResolveFinding (findingId, resolutionState, resolvedBy, note) ->\n                                    return FindingResolved(findingId, resolutionState, resolvedBy, note)\n                                | AddCheckpoint checkpoint -> return CheckpointAdded checkpoint\n                            }\n\n                        let reviewEvent = { Event = reviewEventType; Metadata = metadata }\n                        return! this.ApplyEvent reviewEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/Services.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Azure.Core\nopen Azure.Identity\nopen Azure.Messaging.ServiceBus\nopen Azure.Storage\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Specialized\nopen Azure.Storage.Sas\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Timing\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.AzureEnvironment\nopen Grace.Shared.Constants\nopen Grace.Types.Branch\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Events\nopen Grace.Types.Reference\nopen Grace.Types.Reminder\nopen Grace.Types.Repository\nopen Grace.Types.Organization\nopen Grace.Types.Owner\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Grace.Shared.Utilities\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Azure.Cosmos.Linq\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans.Runtime\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.Linq\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\nopen System.Net.Security\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\nopen System.Threading\nopen System\nopen Microsoft.Extensions.DependencyInjection\nopen System.Runtime.Serialization\nopen System.Reflection\nopen System.Text.RegularExpressions\nopen Microsoft.Azure.Amqp\nopen Azure.Core.Amqp\n\nmodule Services =\n    type ServerGraceIndex = Dictionary<RelativePath, DirectoryVersion>\n    type OwnerIdRecord = { OwnerId: string }\n    type OrganizationIdRecord = { organizationId: string }\n    type RepositoryIdRecord = { repositoryId: string }\n    type BranchIdRecord = { branchId: string }\n\n    type OrganizationDtoValue() =\n        member val public value = OrganizationDto.Default with get, set\n\n    type RepositoryDtoValue() =\n        member val public value = RepositoryDto.Default with get, set\n\n    type BranchDtoValue() =\n        member val public value = BranchDto.Default with get, set\n\n    type BranchIdValue() =\n        member val public branchId = BranchId.Empty with get, set\n\n    type ActorIdValue() =\n        member val public id = String.Empty with get, set\n\n    type OwnerEventValue() =\n        member val public State: OwnerEvent array = Array.Empty<OwnerEvent>() with get, set\n\n    type OrganizationEventValue() =\n        member val public State: OrganizationEvent array = Array.Empty<OrganizationEvent>() with get, set\n\n    type RepositoryEventValue() =\n        member val public State: RepositoryEvent array = Array.Empty<RepositoryEvent>() with get, set\n\n    type BranchEventValue() =\n        member val public State: BranchEvent array = Array.Empty<BranchEvent>() with get, set\n\n    type ReferenceEventValue() =\n        member val public State: ReferenceEvent array = Array.Empty<ReferenceEvent>() with get, set\n\n    type DirectoryVersionEventValue() =\n        member val public State: DirectoryVersionEvent array = Array.Empty<DirectoryVersionEvent>() with get, set\n\n    type DirectoryVersionValue() =\n        member val public value = DirectoryVersion.Default with get, set\n\n    type ReminderValue() =\n        member val public Reminder: ReminderDto = ReminderDto.Default with get, set\n\n    /// Dictionary for caching blob container clients\n    let containerClients = new ConcurrentDictionary<string, BlobContainerClient>()\n\n    /// Shared key credential for Azure Storage when available.\n    let private sharedKeyCredential =\n        lazy\n            AzureEnvironment.storageAccountKey\n            |> Option.map (fun accountKey -> StorageSharedKeyCredential(AzureEnvironment.storageEndpoints.AccountName, accountKey))\n\n    /// Logger instance for the Services.Actor module.\n    let private log = loggerFactory.CreateLogger(\"Services.Actor\")\n\n    let private defaultAzureCredential = lazy (DefaultAzureCredential())\n\n    let private serviceBusClient =\n        lazy\n            let settings = pubSubSettings.AzureServiceBus.Value\n\n            if settings.UseManagedIdentity then\n                let fullyQualifiedNamespace =\n                    if not (String.IsNullOrWhiteSpace settings.FullyQualifiedNamespace) then\n                        settings.FullyQualifiedNamespace\n                    else\n                        AzureEnvironment.tryGetServiceBusFullyQualifiedNamespace ()\n                        |> Option.defaultWith (fun () -> invalidOp \"Azure Service Bus namespace is required for managed identity.\")\n\n                logToConsole $\"Creating ServiceBusClient with Managed Identity for namespace: {fullyQualifiedNamespace}.\"\n                ServiceBusClient(fullyQualifiedNamespace, defaultAzureCredential.Value)\n            else\n                logToConsole \"Creating ServiceBusClient with connection string.\"\n                ServiceBusClient(settings.ConnectionString)\n\n    let private serviceBusSender = lazy (serviceBusClient.Value.CreateSender(pubSubSettings.AzureServiceBus.Value.TopicName))\n\n    /// Publishes a GraceEvent to the configured pub-sub system.\n    let publishGraceEvent (graceEvent: GraceEvent) (metadata: EventMetadata) =\n        task {\n            match pubSubSettings.System with\n            | GracePubSubSystem.AzureServiceBus ->\n                match pubSubSettings.AzureServiceBus with\n                | Some sbSettings ->\n                    try\n                        let payload = JsonSerializer.SerializeToUtf8Bytes(graceEvent, Constants.JsonSerializerOptions)\n                        let message = ServiceBusMessage(payload)\n                        message.ContentType <- \"application/json\"\n                        message.Subject <- \"GraceEvent\"\n                        message.CorrelationId <- metadata.CorrelationId\n                        message.MessageId <- $\"{metadata.CorrelationId}-{getCurrentInstant().ToUnixTimeMilliseconds}\" //Guid.NewGuid().ToString(\"N\")\n                        message.ApplicationProperties[ \"graceEventType\" ] <- getDiscriminatedUnionFullName graceEvent\n\n                        for kvp in metadata.Properties do\n                            message.ApplicationProperties[ kvp.Key ] <- kvp.Value\n\n                        do! serviceBusSender.Value.SendMessageAsync(message)\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Published GraceEvent via Azure Service Bus. CorrelationId: {CorrelationId}; EventType: {EventType}.\",\n                            getCurrentInstantExtended (),\n                            metadata.CorrelationId,\n                            getDiscriminatedUnionCaseName graceEvent\n                        )\n                    with\n                    | ex ->\n                        log.LogError(\n                            ex,\n                            \"{CurrentInstant}: Failed publishing GraceEvent via Azure Service Bus. CorrelationId: {CorrelationId}; EventType: {EventType}.\",\n                            getCurrentInstantExtended (),\n                            metadata.CorrelationId,\n                            getDiscriminatedUnionCaseName graceEvent\n                        )\n                | None ->\n                    log.LogWarning(\n                        \"Azure Service Bus selected but settings were not provided; dropping GraceEvent {EventType}.\",\n                        getDiscriminatedUnionCaseName graceEvent\n                    )\n            | GracePubSubSystem.UnknownPubSubProvider ->\n                log.LogDebug(\n                    \"Pub-sub system disabled; dropping GraceEvent {EventType} with CorrelationId: {CorrelationId}.\",\n                    getDiscriminatedUnionCaseName graceEvent,\n                    metadata.CorrelationId\n                )\n            | otherSystem ->\n                log.LogWarning(\n                    \"Grace pub-sub system {System} not yet implemented; dropping GraceEvent {EventType} with CorrelationId: {CorrelationId}.\",\n                    getDiscriminatedUnionCaseName otherSystem,\n                    getDiscriminatedUnionCaseName graceEvent,\n                    metadata.CorrelationId\n                )\n        }\n        :> Task\n\n    /// Prints a Cosmos DB QueryDefinition with parameters replaced for easier debugging.\n    let printQueryDefinition (queryDefinition: QueryDefinition) =\n        let sb = stringBuilderPool.Get()\n\n        try\n            sb.Append(queryDefinition.QueryText) |> ignore\n\n            queryDefinition.GetQueryParameters()\n            |> Seq.iter (fun struct (name, value: obj) ->\n                match value with\n                | :? int64 as intValue -> sb.Replace(name, $\"{intValue}\")\n                | :? int as intValue -> sb.Replace(name, $\"{intValue}\")\n                | :? double as doubleValue -> sb.Replace(name, $\"{doubleValue}\")\n                | :? bool as boolValue -> sb.Replace(name, $\"{boolValue.ToString().ToLower()}\")\n                | :? single as floatValue -> sb.Replace(name, $\"{floatValue}\")\n                | :? Guid as guidValue -> sb.Replace(name, $\"\\\"{guidValue}\\\"\")\n                | _ -> sb.Replace(name, $\"\\\"{value}\\\"\")\n                |> ignore)\n\n            // Replaces any leading spaces or tabs at the start of each line with four spaces (multiline) and then trims leading/trailing whitespace from the entire multi-line string.\n            let trimmedSql =\n                Regex\n                    .Replace(sb.ToString(), @\"(?m)^[ \\t]+\", \"    \")\n                    .Trim()\n\n            trimmedSql\n        finally\n            stringBuilderPool.Return sb\n\n    /// Custom QueryRequestOptions that requests Index Metrics only in DEBUG build.\n    let queryRequestOptions = QueryRequestOptions()\n#if DEBUG\n    queryRequestOptions.PopulateIndexMetrics <- true\n#endif\n\n    /// Gets an Azure Blob Storage container client for the container that holds the object files for the given repository.\n    let getContainerClient (repositoryDto: RepositoryDto) correlationId =\n        task {\n            let containerName = $\"{repositoryDto.RepositoryId}\"\n            let key = $\"Con:{repositoryDto.StorageAccountName}-{containerName}\"\n\n            let! blobContainerClient =\n                memoryCache.GetOrCreateAsync(\n                    key,\n                    fun cacheEntry ->\n                        task {\n                            let blobContainerClient = Context.blobServiceClient.GetBlobContainerClient(containerName)\n                            let ownerActorProxy = Owner.CreateActorProxy repositoryDto.OwnerId CorrelationId.Empty\n                            let! ownerDto = ownerActorProxy.Get correlationId\n                            let organizationActorProxy = Organization.CreateActorProxy repositoryDto.OrganizationId CorrelationId.Empty\n                            let! organizationDto = organizationActorProxy.Get correlationId\n                            let metadata = Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) :> IDictionary<string, string>\n                            metadata[nameof OwnerId] <- $\"{repositoryDto.OwnerId}\"\n                            metadata[nameof OwnerName] <- $\"{ownerDto.OwnerName}\"\n                            metadata[nameof OrganizationId] <- $\"{repositoryDto.OrganizationId}\"\n                            metadata[nameof OrganizationName] <- $\"{organizationDto.OrganizationName}\"\n                            metadata[nameof RepositoryId] <- $\"{repositoryDto.RepositoryId}\"\n                            metadata[nameof RepositoryName] <- $\"{repositoryDto.RepositoryName}\"\n\n                            let! azureResponse =\n                                blobContainerClient.CreateIfNotExistsAsync(publicAccessType = Models.PublicAccessType.None, metadata = metadata)\n\n                            // This cacheEntry can (and should) last for longer than Grace's default expiration time.\n                            // StorageAccountNames and container names are stable.\n                            // However, we don't want to clog up memoryCache for too long with each client.\n                            // Aiming for a good balance, so keeping it for 10 minutes seems reasonable.\n                            cacheEntry.AbsoluteExpiration <- DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(10.0))\n\n                            return blobContainerClient\n                        }\n                )\n\n            return blobContainerClient\n        }\n\n    /// Gets an Azure Blob Storage client instance for the given repository and file version.\n    let getAzureBlobClient (repositoryDto: RepositoryDto) (blobName: string) (correlationId: CorrelationId) =\n        task {\n            //logToConsole $\"* In getAzureBlobClient; repositoryId: {repositoryDto.RepositoryId}; fileVersion: {fileVersion.RelativePath}.\"\n            let! containerClient = getContainerClient repositoryDto correlationId\n\n            return containerClient.GetBlockBlobClient blobName\n        }\n\n    let getAzureBlobClientForFileVersion (repositoryDto: RepositoryDto) (fileVersion: FileVersion) (correlationId: CorrelationId) =\n        task {\n            let blobName = $\"{fileVersion.RelativePath}/{fileVersion.GetObjectFileName}\"\n            return! getAzureBlobClient repositoryDto blobName correlationId\n        }\n\n    /// Creates a full URI for a specific file version.\n    let private createAzureBlobSasUri (repositoryDto: RepositoryDto) (blobName: string) (permission: BlobSasPermissions) (correlationId: CorrelationId) =\n        task {\n            let! blobContainerClient = getContainerClient repositoryDto correlationId\n\n            let blobSasBuilder =\n                BlobSasBuilder(\n                    permissions = permission,\n                    expiresOn = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(SharedAccessSignatureExpiration)),\n                    StartsOn = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromSeconds(15.0)),\n                    BlobContainerName = blobContainerClient.Name,\n                    BlobName = blobName\n                )\n\n            let! sasUri =\n                if AzureEnvironment.useManagedIdentityForStorage then\n                    task {\n                        // For managed identity, we need to get a user delegation key first\n                        let blobServiceClient = blobContainerClient.GetParentBlobServiceClient()\n\n                        let! userDelegationKey =\n                            blobServiceClient.GetUserDelegationKeyAsync(\n                                DateTimeOffset.UtcNow,\n                                DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(SharedAccessSignatureExpiration))\n                            )\n\n                        let sasQueryParameters = blobSasBuilder.ToSasQueryParameters(userDelegationKey.Value, blobServiceClient.AccountName)\n                        return Uri($\"{blobContainerClient.Uri}/{blobName}?{sasQueryParameters}\")\n                    }\n                else\n                    task {\n                        match sharedKeyCredential.Value with\n                        | Some credential ->\n                            let sasQueryParameters = blobSasBuilder.ToSasQueryParameters(credential)\n                            return Uri($\"{blobContainerClient.Uri}/{blobName}?{sasQueryParameters}\")\n                        | None when blobContainerClient.CanGenerateSasUri ->\n                            return blobContainerClient.GenerateSasUri(blobSasBuilder)\n                        | None ->\n                            return\n                                raise (\n                                    InvalidOperationException(\n                                        \"Azure Blob shared key is not configured and the current blob client cannot generate SAS. Configure grace__azure_storage__key, include AccountKey in grace__azure_storage__connectionstring, or enable managed identity for storage.\"\n                                    )\n                                )\n                    }\n\n            return UriWithSharedAccessSignature($\"{sasUri}\")\n        }\n\n    let azureBlobReadPermissions =\n        (BlobSasPermissions.Read\n         ||| BlobSasPermissions.List) // These are the minimum permissions needed to read a file.\n\n    /// Gets a full Uri, including shared access signature, for reading from the object storage provider.\n    let getUriWithReadSharedAccessSignature (repositoryDto: RepositoryDto) (blobName: string) (correlationId: CorrelationId) =\n        task {\n            match repositoryDto.ObjectStorageProvider with\n            | AzureBlobStorage ->\n                let! sas = createAzureBlobSasUri repositoryDto blobName azureBlobReadPermissions correlationId\n                return sas\n            | AWSS3 -> return UriWithSharedAccessSignature(String.Empty)\n            | GoogleCloudStorage -> return UriWithSharedAccessSignature(String.Empty)\n            | ObjectStorageProvider.Unknown -> return UriWithSharedAccessSignature(String.Empty)\n        }\n\n    /// Gets a full Uri, including shared access signature, for reading from the object storage provider.\n    let getUriWithReadSharedAccessSignatureForFileVersion (repositoryDto: RepositoryDto) (fileVersion: FileVersion) (correlationId: CorrelationId) =\n        task {\n            let blobName = $\"{fileVersion.RelativePath}/{fileVersion.GetObjectFileName}\"\n            return! getUriWithReadSharedAccessSignature repositoryDto blobName correlationId\n        }\n\n    /// The permissions we need to create, write, or tag blobs. Includes read permission to allow for calls to .ExistsAsync().\n    let azureBlobWritePermissions =\n        (BlobSasPermissions.Create\n         ||| BlobSasPermissions.Write\n         ||| BlobSasPermissions.Tag\n         ||| BlobSasPermissions.Read)\n\n    /// Gets a full Uri, including shared access signature, for writing from the object storage provider.\n    let getUriWithWriteSharedAccessSignature (repositoryDto: RepositoryDto) (blobName: string) (correlationId: CorrelationId) =\n        task {\n            match repositoryDto.ObjectStorageProvider with\n            | AWSS3 -> return UriWithSharedAccessSignature(String.Empty)\n            | AzureBlobStorage ->\n                let! sas = createAzureBlobSasUri repositoryDto blobName azureBlobWritePermissions correlationId\n                return sas\n            | GoogleCloudStorage -> return UriWithSharedAccessSignature(String.Empty)\n            | ObjectStorageProvider.Unknown -> return UriWithSharedAccessSignature(String.Empty)\n        }\n\n    /// Gets a full Uri, including shared access signature, for writing from the object storage provider.\n    let getUriWithWriteSharedAccessSignatureForFileVersion (repositoryDto: RepositoryDto) (fileVersion: FileVersion) (correlationId: CorrelationId) =\n        task {\n            let blobName = $\"{fileVersion.RelativePath}/{fileVersion.GetObjectFileName}\"\n            return! getUriWithWriteSharedAccessSignature repositoryDto blobName correlationId\n        }\n\n    /// Checks whether an owner name exists in the system.\n    let ownerNameExists (ownerName: string) cacheResultIfNotFound (correlationId: CorrelationId) =\n        task {\n            let ownerNameActorProxy = OwnerName.CreateActorProxy ownerName correlationId\n\n            match! ownerNameActorProxy.GetOwnerId correlationId with\n            | Some ownerId -> return true\n            | None ->\n                // Check if the owner name exists in the database.\n                // If it does, we need to add it to the memoryCache, and store the ownerId in the OwnerNameActor.\n                // If it does not, we need to add it to the memoryCache with a false value.\n                // We have to call into Actor storage to get the OwnerId.\n                match actorStateStorageProvider with\n                | Unknown -> return false\n                | AzureCosmosDb ->\n                    let owners = List<OwnerDto>()\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE (STRINGEQUALS(c.State[0].Event.created.ownerName, @ownerName, true)\n                                OR STRINGEQUALS(c.State[0].Event.setName.ownerName, @ownerName, true))\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                            .WithParameter(\"@ownerName\", ownerName)\n                            .WithParameter(\"@grainType\", StateName.Owner)\n                            .WithParameter(\"@partitionKey\", StateName.Owner)\n\n                    //logToConsole $\"QueryDefinition in ownerNameExists:{Environment.NewLine}{printQueryDefinition queryDefinition}\"\n\n                    let iterator =\n                        DefaultRetryPolicy.Execute (fun () ->\n                            cosmosContainer.GetItemQueryIterator<OwnerEventValue>(queryDefinition, requestOptions = queryRequestOptions))\n\n                    while iterator.HasMoreResults do\n                        let! result = iterator.ReadNextAsync()\n                        let ownersThatMatchName = result.Resource\n\n                        ownersThatMatchName\n                        |> Seq.iter (fun eventsForOneOwner ->\n                            let ownerDto =\n                                eventsForOneOwner.State\n                                |> Seq.fold (fun ownerDto ownerEvent -> ownerDto |> OwnerDto.UpdateDto ownerEvent) OwnerDto.Default\n\n                            owners.Add(ownerDto))\n\n                    let ownerWithName =\n                        owners.FirstOrDefault(\n                            (fun owner -> String.Equals(owner.OwnerName, ownerName, StringComparison.InvariantCultureIgnoreCase)),\n                            OwnerDto.Default\n                        )\n\n                    if String.IsNullOrEmpty(ownerWithName.OwnerName) then\n                        logToConsole $\"Did not find ownerId using OwnerName {ownerName}. cacheResultIfNotFound: {cacheResultIfNotFound}.\"\n                        // We didn't find the OwnerId, so add this OwnerName to the MemoryCache and indicate that we have already checked.\n                        //use newCacheEntry =\n                        //    memoryCache.CreateEntry(\n                        //        $\"OwN:{ownerName}\",\n                        //        Value = MemoryCache.EntityDoesNotExist,\n                        //        AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                        //    )\n                        return false\n                    else\n                        // Add this OwnerName and OwnerId to the MemoryCache.\n                        memoryCache.CreateOwnerNameEntry ownerName ownerWithName.OwnerId\n                        memoryCache.CreateOwnerIdEntry ownerWithName.OwnerId MemoryCache.Exists\n\n                        // Set the OwnerId in the OwnerName actor.\n                        do! ownerNameActorProxy.SetOwnerId ownerWithName.OwnerId correlationId\n                        return true\n                | MongoDB -> return false\n\n        }\n\n    /// Checks whether an organization has been deleted by querying the actor, and updates the MemoryCache with the result.\n    let ownerIsDeleted (ownerId: string) correlationId =\n        task {\n            let ownerGuid = OwnerId.Parse(ownerId)\n            let ownerActorProxy = Owner.CreateActorProxy ownerGuid correlationId\n\n            let! isDeleted = ownerActorProxy.IsDeleted correlationId\n\n            if isDeleted then\n                memoryCache.CreateDeletedOwnerIdEntry ownerGuid MemoryCache.DoesNotExist\n                return Some ownerId\n            else\n                memoryCache.CreateDeletedOwnerIdEntry ownerGuid MemoryCache.Exists\n                return None\n        }\n\n    /// Checks whether an owner exists by querying the actor, and updates the MemoryCache with the result.\n    let ownerExists (ownerId: string) correlationId =\n        task {\n            // Call the Owner actor to check if the owner exists.\n            let ownerGuid = Guid.Parse(ownerId)\n            let ownerActorProxy = Owner.CreateActorProxy ownerGuid correlationId\n\n            let! exists = ownerActorProxy.Exists correlationId\n\n            if exists then\n                // Add this OwnerId to the MemoryCache.\n                memoryCache.CreateOwnerIdEntry ownerGuid MemoryCache.Exists\n                return Some ownerId\n            else\n                return None\n        }\n\n    /// Gets the OwnerId by checking for the existence of OwnerId if provided, or searching by OwnerName.\n    let resolveOwnerId (ownerId: string) (ownerName: string) (correlationId: CorrelationId) =\n        task {\n            let mutable ownerGuid = Guid.Empty\n\n            if\n                not <| String.IsNullOrEmpty(ownerId)\n                && Guid.TryParse(ownerId, &ownerGuid)\n            then\n                // Check if we have this owner id in MemoryCache.\n                match memoryCache.GetOwnerIdEntry ownerGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.Exists -> return Some ownerId\n                    | MemoryCache.DoesNotExist -> return None\n                    | _ -> return! ownerExists ownerId correlationId\n                | None -> return! ownerExists ownerId correlationId\n            elif String.IsNullOrEmpty(ownerName) then\n                // We have no OwnerId or OwnerName to resolve.\n                return None\n            else\n                // Check if we have this owner name in MemoryCache.\n                match memoryCache.GetOwnerNameEntry ownerName with\n                | Some ownerGuid ->\n                    // We have already checked and the owner exists.\n                    memoryCache.CreateOwnerIdEntry ownerGuid MemoryCache.Exists\n                    return Some $\"{ownerGuid}\"\n                | None ->\n                    // Check if we have an active OwnerName actor with a cached result.\n                    let ownerNameActorProxy = OwnerName.CreateActorProxy ownerName correlationId\n\n                    match! ownerNameActorProxy.GetOwnerId correlationId with\n                    | Some ownerId ->\n                        // Add this OwnerName and OwnerId to the MemoryCache.\n                        memoryCache.CreateOwnerNameEntry ownerName ownerId\n                        memoryCache.CreateOwnerIdEntry ownerGuid MemoryCache.Exists\n\n                        return Some $\"{ownerId}\"\n                    | None ->\n                        let! nameExists = ownerNameExists ownerName true correlationId\n\n                        if nameExists then\n                            // We have already checked and the owner exists.\n                            match memoryCache.GetOwnerNameEntry ownerName with\n                            | Some ownerGuid -> return Some $\"{ownerGuid}\"\n                            | None -> // This should never happen, because we just populated the cache in ownerNameExists.\n                                return None\n                        else\n                            // The owner name does not exist.\n                            return None\n        }\n\n    /// Checks whether an organization is deleted by querying the actor, and updates the MemoryCache with the result.\n    let organizationIsDeleted (organizationId: string) correlationId =\n        task {\n            let organizationGuid = Guid.Parse(organizationId)\n            let organizationActorProxy = Organization.CreateActorProxy organizationGuid correlationId\n\n            let! isDeleted = organizationActorProxy.IsDeleted correlationId\n\n            let organizationGuid = OrganizationId.Parse(organizationId)\n\n            if isDeleted then\n                memoryCache.CreateDeletedOrganizationIdEntry organizationGuid MemoryCache.DoesNotExist\n                return Some organizationId\n            else\n                memoryCache.CreateDeletedOrganizationIdEntry organizationGuid MemoryCache.Exists\n                return None\n        }\n\n    /// Checks whether an organization exists by querying the actor, and updates the MemoryCache with the result.\n    let organizationExists (organizationId: string) correlationId =\n        task {\n            // Call the Organization actor to check if the organization exists.\n            let organizationGuid = Guid.Parse(organizationId)\n            let organizationActorProxy = Organization.CreateActorProxy organizationGuid correlationId\n\n            let! exists = organizationActorProxy.Exists correlationId\n\n            if exists then\n                // Add this OrganizationId to the MemoryCache.\n                memoryCache.CreateOrganizationIdEntry (OrganizationId.Parse(organizationId)) MemoryCache.Exists\n                return Some organizationId\n            else\n                return None\n        }\n\n    /// Gets the OrganizationId by either returning OrganizationId if provided, or searching by OrganizationName.\n    let resolveOrganizationId (ownerId: OwnerId) (organizationId: string) (organizationName: string) (correlationId: CorrelationId) =\n        task {\n            let mutable organizationGuid = Guid.Empty\n\n            if\n                not <| String.IsNullOrEmpty(organizationId)\n                && Guid.TryParse(organizationId, &organizationGuid)\n            then\n                match memoryCache.GetOrganizationIdEntry organizationGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.Exists -> return Some organizationId\n                    | MemoryCache.DoesNotExist -> return None\n                    | _ -> return! organizationExists organizationId correlationId\n                | None -> return! organizationExists organizationId correlationId\n            elif String.IsNullOrEmpty(organizationName) then\n                // We have no OrganizationId or OrganizationName to resolve.\n                return None\n            else\n                // Check if we have this organization name in MemoryCache.\n                match memoryCache.GetOrganizationNameEntry organizationName with\n                | Some organizationGuid ->\n                    if organizationGuid.Equals(MemoryCache.EntityDoesNotExistGuid) then\n                        // We have already checked and the organization does not exist.\n                        return None\n                    else\n                        memoryCache.CreateOrganizationIdEntry organizationGuid MemoryCache.Exists\n                        return Some $\"{organizationGuid}\"\n                | None ->\n                    // Check if we have an active OrganizationName actor with a cached result.\n                    let organizationNameActorProxy = OrganizationName.CreateActorProxy ownerId organizationName correlationId\n\n                    match! organizationNameActorProxy.GetOrganizationId correlationId with\n                    | Some organizationId ->\n                        // Add this OrganizationName and OrganizationId to the MemoryCache.\n                        memoryCache.CreateOrganizationNameEntry organizationName organizationId\n                        memoryCache.CreateOrganizationIdEntry organizationId MemoryCache.Exists\n                        return Some $\"{organizationId}\"\n                    | None ->\n                        // We have to call into Actor storage to get the OrganizationId.\n                        match actorStateStorageProvider with\n                        | Unknown -> return None\n                        | AzureCosmosDb ->\n                            let queryDefinition =\n                                QueryDefinition(\n                                    \"\"\"\n                                    SELECT c.State[0].Event.created.organizationId AS OrganizationId\n                                    FROM c\n                                    WHERE STRINGEQUALS(c.State[0].Event.created.organizationName, @organizationName, true)\n                                        AND STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                        AND c.GrainType = @grainType\n                                        AND c.PartitionKey = @partitionKey\n                                    \"\"\"\n                                )\n                                    .WithParameter(\"@organizationName\", organizationName)\n                                    .WithParameter(\"@ownerId\", ownerId)\n                                    .WithParameter(\"@grainType\", StateName.Organization)\n                                    .WithParameter(\"@partitionKey\", StateName.Organization)\n\n                            let iterator = DefaultRetryPolicy.Execute(fun () -> cosmosContainer.GetItemQueryIterator<OrganizationIdRecord>(queryDefinition))\n\n                            if iterator.HasMoreResults then\n                                let! currentResultSet = iterator.ReadNextAsync()\n\n                                let organizationId =\n                                    currentResultSet\n                                        .FirstOrDefault(\n                                            { organizationId = String.Empty }\n                                        )\n                                        .organizationId\n\n                                if String.IsNullOrEmpty(organizationId) then\n                                    // We didn't find the OrganizationId, so add this OrganizationName to the MemoryCache and indicate that we have already checked.\n                                    memoryCache.CreateOrganizationNameEntry organizationName MemoryCache.EntityDoesNotExistGuid\n                                    return None\n                                else\n                                    // Add this OrganizationName and OrganizationId to the MemoryCache.\n                                    organizationGuid <- Guid.Parse(organizationId)\n                                    memoryCache.CreateOrganizationNameEntry organizationName organizationGuid\n                                    memoryCache.CreateOrganizationIdEntry organizationGuid MemoryCache.Exists\n\n                                    do! organizationNameActorProxy.SetOrganizationId organizationGuid correlationId\n                                    return Some organizationId\n                            else\n                                return None\n                        | MongoDB -> return None\n        }\n\n    /// Checks whether a repository has been deleted by querying the actor, and updates the MemoryCache with the result.\n    let repositoryIsDeleted organizationId (repositoryId: string) correlationId =\n        task {\n            // Call the Repository actor to check if the repository is deleted.\n            let repositoryGuid = Guid.Parse(repositoryId)\n            let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryGuid correlationId\n\n            let! isDeleted = repositoryActorProxy.IsDeleted correlationId\n\n            if isDeleted then\n                memoryCache.CreateDeletedRepositoryIdEntry repositoryGuid MemoryCache.DoesNotExist\n                return Some repositoryId\n            else\n                memoryCache.CreateDeletedRepositoryIdEntry repositoryGuid MemoryCache.Exists\n                return None\n        }\n\n    /// Checks whether a repository exists by querying the actor, and updates the MemoryCache with the result.\n    let repositoryExists organizationId (repositoryId: string) correlationId =\n        task {\n            // Call the Repository actor to check if the repository exists.\n            let repositoryGuid = Guid.Parse(repositoryId)\n            let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryGuid correlationId\n\n            let! exists = repositoryActorProxy.Exists correlationId\n\n            if exists then\n                // Add this RepositoryId to the MemoryCache.\n                memoryCache.CreateRepositoryIdEntry repositoryGuid MemoryCache.Exists\n                return Some repositoryGuid\n            else\n                return None\n        }\n\n    /// Gets the RepositoryId by returning RepositoryId if provided, or searching by RepositoryName within the provided owner and organization.\n    let resolveRepositoryId (ownerId: OwnerId) (organizationId: OrganizationId) (repositoryId: string) (repositoryName: string) (correlationId: CorrelationId) =\n        task {\n            let mutable repositoryGuid = Guid.Empty\n\n            if\n                not <| String.IsNullOrEmpty(repositoryId)\n                && Guid.TryParse(repositoryId, &repositoryGuid)\n            then\n                match memoryCache.GetRepositoryIdEntry repositoryGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.Exists -> return Some repositoryGuid\n                    | MemoryCache.DoesNotExist -> return None\n                    | _ -> return! repositoryExists organizationId repositoryId correlationId\n                | None -> return! repositoryExists organizationId repositoryId correlationId\n            elif String.IsNullOrEmpty(repositoryName) then\n                // We don't have a RepositoryId or RepositoryName, so we can't resolve the RepositoryId.\n                return None\n            else\n                match memoryCache.GetRepositoryNameEntry repositoryName with\n                | Some repositoryGuid ->\n                    if repositoryGuid.Equals(Constants.MemoryCache.EntityDoesNotExist) then\n                        // We have already checked and the repository does not exist.\n                        return None\n                    else\n                        // We have already checked and the repository exists.\n                        memoryCache.CreateRepositoryIdEntry repositoryGuid MemoryCache.Exists\n                        return Some repositoryGuid\n                | None ->\n                    // Check if we have an active RepositoryName actor with a cached result.\n                    let repositoryNameActorProxy = RepositoryName.CreateActorProxy ownerId organizationId repositoryName correlationId\n\n                    match! repositoryNameActorProxy.GetRepositoryId correlationId with\n                    | Some repositoryId ->\n                        memoryCache.CreateRepositoryNameEntry repositoryName repositoryId\n                        memoryCache.CreateRepositoryIdEntry repositoryId MemoryCache.Exists\n                        return Some repositoryId\n                    | None ->\n                        // We have to call into Actor storage to get the RepositoryId.\n                        match actorStateStorageProvider with\n                        | Unknown -> return None\n                        | AzureCosmosDb ->\n                            let queryDefinition =\n                                QueryDefinition(\n                                    \"\"\"\n                                    SELECT c.State[0].Event.created.repositoryId AS RepositoryId\n                                    FROM c\n                                    WHERE STRINGEQUALS(c.State[0].Event.created.repositoryName, @repositoryName, true)\n                                        AND STRINGEQUALS(c.State[0].Event.created.organizationId, @organizationId, true)\n                                        AND STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                        AND c.GrainType = @grainType\n                                        AND c.PartitionKey = @partitionKey\n                                   \"\"\"\n                                )\n                                    .WithParameter(\"@repositoryName\", repositoryName)\n                                    .WithParameter(\"@organizationId\", organizationId)\n                                    .WithParameter(\"@ownerId\", ownerId)\n                                    .WithParameter(\"@grainType\", StateName.Repository)\n                                    .WithParameter(\"@partitionKey\", organizationId)\n\n                            let iterator = cosmosContainer.GetItemQueryIterator<RepositoryIdRecord>(queryDefinition)\n\n                            if iterator.HasMoreResults then\n                                let! currentResultSet = iterator.ReadNextAsync()\n\n                                let repositoryIdString =\n                                    currentResultSet\n                                        .FirstOrDefault(\n                                            { repositoryId = String.Empty }\n                                        )\n                                        .repositoryId\n\n                                if String.IsNullOrEmpty(repositoryIdString) then\n                                    // We didn't find the RepositoryId, so add this RepositoryName to the MemoryCache and indicate that we have already checked.\n                                    memoryCache.CreateRepositoryNameEntry repositoryName MemoryCache.EntityDoesNotExistGuid\n                                    return None\n                                else\n                                    // Add this RepositoryName and RepositoryId to the MemoryCache.\n                                    let repositoryId = Guid.Parse(repositoryIdString)\n                                    memoryCache.CreateRepositoryNameEntry repositoryName repositoryId\n                                    memoryCache.CreateRepositoryIdEntry repositoryId MemoryCache.Exists\n\n                                    // Set the RepositoryId in the RepositoryName actor.\n                                    do! repositoryNameActorProxy.SetRepositoryId repositoryId correlationId\n                                    return Some repositoryId\n                            else\n                                return None\n                        | MongoDB -> return None\n        }\n\n    /// Checks whether a branch has been deleted by querying the actor, and updates the MemoryCache with the result.\n    let branchIsDeleted (branchId: string) repositoryId correlationId =\n        task {\n            let branchGuid = Guid.Parse(branchId)\n            let branchActorProxy = Branch.CreateActorProxy branchGuid repositoryId correlationId\n\n            let! isDeleted = branchActorProxy.IsDeleted correlationId\n\n            if isDeleted then\n                memoryCache.CreateDeletedBranchIdEntry branchGuid MemoryCache.DoesNotExist\n                return Some branchId\n            else\n                memoryCache.CreateDeletedBranchIdEntry branchGuid MemoryCache.Exists\n                return None\n        }\n\n    /// Checks whether a branch exists by querying the actor, and updates the MemoryCache with the result.\n    let branchExists (branchId: BranchId) repositoryId correlationId =\n        task {\n            // Call the Branch actor to check if the branch exists.\n            let branchActorProxy = Branch.CreateActorProxy branchId repositoryId correlationId\n            let! exists = branchActorProxy.Exists correlationId\n\n            if exists then\n                // Add this BranchId to the MemoryCache.\n                memoryCache.CreateBranchIdEntry branchId MemoryCache.Exists\n                return Some branchId\n            else\n                return None\n        }\n\n    /// Gets the BranchId by returning BranchId if provided, or searching by BranchName within the provided repository.\n    let resolveBranchId ownerId organizationId (repositoryId: RepositoryId) branchIdString branchName (correlationId: CorrelationId) =\n        task {\n            let mutable branchGuid = Guid.Empty\n\n            if\n                not <| String.IsNullOrEmpty(branchIdString)\n                && Guid.TryParse(branchIdString, &branchGuid)\n            then\n                // We have a BranchId, so check if it exists.\n                match memoryCache.GetBranchIdEntry branchGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.Exists -> return Some branchGuid\n                    | MemoryCache.DoesNotExist -> return None\n                    | _ -> return! branchExists branchGuid repositoryId correlationId\n                | None -> return! branchExists branchGuid repositoryId correlationId\n            elif String.IsNullOrEmpty(branchName) then\n                // We don't have a BranchId or BranchName, so we can't resolve the BranchId.\n                return None\n            else\n                // We have no BranchId, but we do have a BranchName.\n                // Check if we have an active BranchName actor with a cached result.\n                match memoryCache.GetBranchNameEntry(repositoryId, branchName) with\n                | Some branchGuid ->\n                    // We have a cached result.\n                    if branchGuid.Equals(Constants.MemoryCache.EntityDoesNotExist) then\n                        // We have already checked and the branch does not exist.\n                        return None\n                    else\n                        // We have already checked and the branch exists.\n                        return Some branchGuid\n                | None ->\n                    // The BranchName was not in the MemoryCache on this node, but we may have it in a BranchName actor.\n                    let branchNameActorProxy = BranchName.CreateActorProxy repositoryId branchName correlationId\n\n                    match! branchNameActorProxy.GetBranchId correlationId with\n                    | Some branchId ->\n                        // We have an active BranchName actor with the BranchId cached.\n                        memoryCache.CreateBranchNameEntry(repositoryId, branchName, branchId)\n                        memoryCache.CreateBranchIdEntry branchId MemoryCache.Exists\n                        return Some branchId\n                    | None ->\n                        // The BranchName actor was not active, so we have to search the database.\n                        match actorStateStorageProvider with\n                        | Unknown -> return None\n                        | AzureCosmosDb ->\n                            let queryDefinition =\n                                QueryDefinition(\n                                    \"\"\"\n                                    SELECT c.State[0].Event.created.branchId AS BranchId\n                                    FROM c\n                                    WHERE STRINGEQUALS(c.State[0].Event.created.branchName, @branchName, true)\n                                        AND STRINGEQUALS(c.State[0].Event.created.organizationId, @organizationId, true)\n                                        AND STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                        AND c.GrainType = @grainType\n                                        AND c.PartitionKey = @partitionKey\n                                    \"\"\"\n                                )\n                                    .WithParameter(\"@branchName\", branchName)\n                                    .WithParameter(\"@organizationId\", organizationId)\n                                    .WithParameter(\"@ownerId\", ownerId)\n                                    .WithParameter(\"@grainType\", StateName.Branch)\n                                    .WithParameter(\"@partitionKey\", repositoryId)\n\n                            let iterator = DefaultRetryPolicy.Execute(fun () -> cosmosContainer.GetItemQueryIterator<BranchIdRecord>(queryDefinition))\n                            //logToConsole $\"QueryDefinition in resolveBranchId:{Environment.NewLine}{printQueryDefinition queryDefinition}\"\n\n                            if iterator.HasMoreResults then\n                                let! currentResultSet = iterator.ReadNextAsync()\n\n                                let branchId =\n                                    currentResultSet\n                                        .FirstOrDefault(\n                                            { branchId = String.Empty }\n                                        )\n                                        .branchId\n\n                                if String.IsNullOrEmpty(branchId) then\n                                    // We didn't find the BranchId.\n                                    return None\n                                else\n                                    // Add this BranchName and BranchId to the MemoryCache.\n                                    branchGuid <- Guid.Parse(branchId)\n\n                                    // Add this BranchName and BranchId to the MemoryCache.\n                                    memoryCache.CreateBranchNameEntry(repositoryId, branchName, branchGuid)\n                                    memoryCache.CreateBranchIdEntry branchGuid MemoryCache.Exists\n\n                                    // Set the BranchId in the BranchName actor.\n                                    do! branchNameActorProxy.SetBranchId branchGuid correlationId\n                                    //logToConsole $\"BranchName actor was not active. BranchName: {branchName}; BranchId: {branchGuid}.\"\n                                    return Some branchGuid\n                            else\n                                return None\n                        | MongoDB -> return None\n        }\n\n    /// Creates a CosmosDB SQL WHERE clause that includes or excludes deleted entities.\n    let includeDeletedEntitiesClause includeDeletedEntities =\n        if includeDeletedEntities then\n            // If includeDeletedEntities is true, we don't need to filter out deleted entities.\n            String.Empty\n        else\n            // We're checking to see if:\n            //  (count of logicalDeletes) = (count of undeletes)\n            //\n            //  Usually, of course, both counts are 0, but it's not as simple as checking if there's a delete event.\n            //  We can tell if an entity is deleted by checking those counts.\n            \"\"\"\n            AND (\n            (SELECT VALUE COUNT(1)\n                FROM c JOIN e in c.State\n                WHERE IS_DEFINED(e.Event.logicalDeleted)) <=\n            (SELECT VALUE COUNT(1)\n                FROM c JOIN e in c.State\n                WHERE IS_DEFINED(e.Event.undeleted))\n            )\n            \"\"\"\n\n    /// Gets a list of organizations for the specified owner.\n    let getOrganizations (ownerId: OwnerId) (maxCount: int) includeDeleted =\n        task {\n            let organizations = List<OrganizationDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    try\n                        let queryDefinition =\n                            QueryDefinition(\n                                $\"\"\"\n                                SELECT TOP @maxCount c.State\n                                FROM c\n                                WHERE STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                    AND c.GrainType = @grainType\n                                    AND c.PartitionKey = @partitionKey\n                                    {includeDeletedEntitiesClause includeDeleted}\n                                \"\"\"\n                            )\n                                .WithParameter(\"@maxCount\", maxCount)\n                                .WithParameter(\"@ownerId\", ownerId)\n                                .WithParameter(\"@grainType\", StateName.Organization)\n                                .WithParameter(\"@partitionKey\", StateName.Organization)\n\n                        let iterator = cosmosContainer.GetItemQueryIterator<OrganizationEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                        while iterator.HasMoreResults do\n                            let! results = iterator.ReadNextAsync()\n\n                            indexMetrics.Append($\"{results.IndexMetrics}, \")\n                            |> ignore\n\n                            requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                            |> ignore\n\n                            let eventsForAllOrganizations = results.Resource\n\n                            eventsForAllOrganizations\n                            |> Seq.iter (fun eventsForOneOrganization ->\n                                let organizationDto =\n                                    eventsForOneOrganization.State\n                                    |> Seq.fold\n                                        (fun organizationDto organizationEvent ->\n                                            organizationDto\n                                            |> OrganizationDto.UpdateDto organizationEvent)\n                                        OrganizationDto.Default\n\n                                organizations.Add(organizationDto))\n\n                        if (indexMetrics.Length >= 2)\n                           && (requestCharge.Length >= 2)\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                            |> ignore\n                    with\n                    | ex ->\n                        logToConsole $\"Got an exception.\"\n                        logToConsole $\"{ExceptionResponse.Create ex}\"\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            return\n                organizations\n                    .OrderBy(fun o -> o.OrganizationName)\n                    .ToArray()\n        }\n\n    /// Checks if the specified organization name is unique for the specified owner.\n    let organizationNameIsUnique<'T> (ownerId: string) (organizationName: string) (correlationId: CorrelationId) =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return Ok false\n            | AzureCosmosDb ->\n                try\n                    let organizations = List<OrganizationDto>()\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE (STRINGEQUALS(c.State[0].Event.created.organizationName, @organizationName, true)\n                                OR STRINGEQUALS(c.State[0].Event.setName.organizationName, @organizationName, true))\n                                AND STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                            .WithParameter(\"@organizationName\", organizationName)\n                            .WithParameter(\"@ownerId\", ownerId)\n                            .WithParameter(\"@grainType\", StateName.Organization)\n                            .WithParameter(\"@partitionKey\", StateName.Organization)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<OrganizationEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! result = iterator.ReadNextAsync()\n                        let eventsForAllOrganizations = result.Resource\n\n                        eventsForAllOrganizations\n                        |> Seq.iter (fun eventsForOneOrganization ->\n                            let organizationDto =\n                                eventsForOneOrganization.State\n                                |> Seq.fold\n                                    (fun organizationDto organizationEvent ->\n                                        organizationDto\n                                        |> OrganizationDto.UpdateDto organizationEvent)\n                                    OrganizationDto.Default\n\n                            organizations.Add(organizationDto))\n\n                    let organizationWithName =\n                        organizations.FirstOrDefault(\n                            (fun o -> String.Equals(o.OrganizationName, organizationName, StringComparison.OrdinalIgnoreCase)),\n                            OrganizationDto.Default\n                        )\n\n                    if String.IsNullOrEmpty(organizationWithName.OrganizationName) then\n                        // The organization name is unique.\n                        return Ok true\n                    else\n                        // The organization name is not unique.\n                        return Ok false\n                with\n                | ex -> return Error $\"{ExceptionResponse.Create ex}\"\n            | MongoDB -> return Ok false\n        }\n\n    /// Checks if the specified repository name is unique for the specified organization.\n    let repositoryNameIsUnique<'T> (ownerId: string) (organizationId: string) (repositoryName: string) (correlationId: CorrelationId) =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return Ok false\n            | AzureCosmosDb ->\n                try\n                    let repositories = List<RepositoryDto>()\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE (STRINGEQUALS(c.State[0].Event.created.repositoryName, @repositoryName, true)\n                                OR STRINGEQUALS(c.State[0].Event.setName.repositoryName, @repositoryName, true))\n                                AND STRINGEQUALS(c.State[0].Event.created.organizationId, @organizationId, true)\n                                AND STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                            .WithParameter(\"@repositoryName\", repositoryName)\n                            .WithParameter(\"@organizationId\", organizationId)\n                            .WithParameter(\"@ownerId\", ownerId)\n                            .WithParameter(\"@grainType\", StateName.Repository)\n                            .WithParameter(\"@partitionKey\", organizationId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<RepositoryEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! result = iterator.ReadNextAsync()\n                        let eventsForAllRepositories = result.Resource\n\n                        eventsForAllRepositories\n                        |> Seq.iter (fun eventsForOneRepository ->\n                            let repositoryDto =\n                                eventsForOneRepository.State\n                                |> Seq.fold\n                                    (fun repositoryDto repositoryEvent ->\n                                        repositoryDto\n                                        |> RepositoryDto.UpdateDto repositoryEvent)\n                                    RepositoryDto.Default\n\n                            repositories.Add(repositoryDto))\n\n                    let repositoryWithName =\n                        repositories.FirstOrDefault(\n                            (fun o -> String.Equals(o.RepositoryName, repositoryName, StringComparison.OrdinalIgnoreCase)),\n                            RepositoryDto.Default\n                        )\n\n                    if String.IsNullOrEmpty(repositoryWithName.RepositoryName) then\n                        // The repository name is unique.\n                        return Ok true\n                    else\n                        return Ok true // This else should never be hit.\n                with\n                | ex -> return Error $\"{ExceptionResponse.Create ex}\"\n            | MongoDB -> return Ok false\n        }\n\n    /// Gets a list of repositories for the specified organization.\n    let getRepositories (ownerId: OwnerId) (organizationId: OrganizationId) (maxCount: int) includeDeleted =\n        task {\n            let repositories = List<RepositoryDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    try\n                        let queryDefinition =\n                            QueryDefinition(\n                                $\"\"\"\n                                SELECT TOP @maxCount c.State\n                                FROM c\n                                WHERE STRINGEQUALS(c.State[0].Event.created.organizationId, @organizationId, true)\n                                    AND STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                    {includeDeletedEntitiesClause includeDeleted}\n                                    AND c.GrainType = @grainType\n                                    AND c.PartitionKey = @partitionKey\n                                \"\"\"\n                            )\n                                .WithParameter(\"@ownerId\", ownerId)\n                                .WithParameter(\"@organizationId\", organizationId)\n                                .WithParameter(\"@maxCount\", maxCount)\n                                .WithParameter(\"@grainType\", StateName.Repository)\n                                .WithParameter(\"@partitionKey\", organizationId)\n\n                        let iterator = cosmosContainer.GetItemQueryIterator<RepositoryEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                        while iterator.HasMoreResults do\n                            let! results = iterator.ReadNextAsync()\n\n                            indexMetrics.Append($\"{results.IndexMetrics}, \")\n                            |> ignore\n\n                            requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                            |> ignore\n\n                            let eventsForAllRepositories = results.Resource\n\n                            eventsForAllRepositories\n                            |> Seq.iter (fun eventsForOneRepository ->\n                                let repositoryDto =\n                                    eventsForOneRepository.State\n                                    |> Array.fold\n                                        (fun repositoryDto repositoryEvent ->\n                                            repositoryDto\n                                            |> RepositoryDto.UpdateDto repositoryEvent)\n                                        RepositoryDto.Default\n\n                                repositories.Add(repositoryDto))\n\n                        if (indexMetrics.Length >= 2)\n                           && (requestCharge.Length >= 2)\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                            |> ignore\n                    with\n                    | ex ->\n                        logToConsole $\"Got an exception.\"\n                        logToConsole $\"{ExceptionResponse.Create ex}\"\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            return\n                repositories\n                    .OrderBy(fun r -> r.RepositoryName)\n                    .ToArray()\n        }\n\n    let internal isNotDeletedReference (referenceDto: ReferenceDto) = referenceDto.DeletedAt.IsNone\n\n    let internal hasPromotionSetTerminalLink (referenceDto: ReferenceDto) =\n        referenceDto.Links\n        |> Seq.exists (fun link ->\n            match link with\n            | ReferenceLinkType.PromotionSetTerminal _ -> true\n            | _ -> false)\n\n    let internal tryGetLatestNotDeletedReference (references: seq<ReferenceDto>) = references |> Seq.tryFind isNotDeletedReference\n\n    let internal tryGetLatestEffectivePromotionReference (references: seq<ReferenceDto>) =\n        references\n        |> Seq.tryFind (fun referenceDto ->\n            isNotDeletedReference referenceDto\n            && hasPromotionSetTerminalLink referenceDto)\n\n    /// Gets a list of references that match a provided SHA-256 hash.\n    let getReferencesBySha256Hash (repositoryId: RepositoryId) (branchId: BranchId) (sha256Hash: Sha256Hash) (maxCount: int) =\n        task {\n            let references = List<ReferenceDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP @maxCount c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND STARTSWITH(c.State[0].Event.created.Sha256Hash, @sha256Hash, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@sha256Hash\", sha256Hash)\n                            .WithParameter(\"@branchId\", branchId)\n                            .WithParameter(\"@grainType\", StateName.Reference)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync()\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllReferences = results.Resource\n\n                        eventsForAllReferences\n                        |> Seq.iter (fun eventsForOneReference ->\n                            let referenceDto =\n                                eventsForOneReference.State\n                                |> Array.fold\n                                    (fun referenceDto referenceEvent ->\n                                        referenceDto\n                                        |> ReferenceDto.UpdateDto referenceEvent)\n                                    ReferenceDto.Default\n\n                            if isNotDeletedReference referenceDto then references.Add(referenceDto))\n\n                    if (indexMetrics.Length >= 2)\n                       && (requestCharge.Length >= 2)\n                       && Activity.Current <> null then\n                        Activity\n                            .Current\n                            .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                            .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            return\n                references\n                    .OrderBy(fun reference -> reference.CreatedAt)\n                    .ToArray()\n        }\n\n    /// Gets a reference by its SHA-256 hash.\n    let getReferenceBySha256Hash (repositoryId: RepositoryId) (branchId: BranchId) (sha256Hash: Sha256Hash) =\n        task {\n            let! references = getReferencesBySha256Hash repositoryId branchId sha256Hash 1\n            if references.Length > 0 then return Some references[0] else return None\n        }\n\n    /// Gets a list of references for a given branch.\n    let getReferences (repositoryId: RepositoryId) (branchId: BranchId) (maxCount: int) (correlationId: CorrelationId) =\n        task {\n            let references = List<ReferenceDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP @maxCount c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@branchId\", branchId)\n                            .WithParameter(\"@grainType\", StateName.Reference)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        addTiming TimingFlag.BeforeStorageQuery \"getReferences\" correlationId\n                        let! results = iterator.ReadNextAsync()\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllReferences = results.Resource\n\n                        eventsForAllReferences\n                        |> Seq.iter (fun eventsForOneReference ->\n                            let referenceDto =\n                                eventsForOneReference.State\n                                |> Array.fold\n                                    (fun referenceDto referenceEvent ->\n                                        referenceDto\n                                        |> ReferenceDto.UpdateDto referenceEvent)\n                                    ReferenceDto.Default\n\n                            references.Add(referenceDto))\n\n                    //logToConsole\n                    //    $\"In Services.Actor.getReferences: BranchId: {branchId}; RepositoryId: {repositoryId}; Retrieved {references.Count} references.{Environment.NewLine}{printQueryDefinition queryDefinition}{Environment.NewLine}{serialize references}\"\n\n                    if indexMetrics.Length >= 2\n                       && requestCharge.Length >= 2\n                       && Activity.Current <> null then\n                        Activity.Current.SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                        |> ignore\n\n                        Activity.Current.SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            return\n                references\n                    .OrderBy(fun reference -> reference.CreatedAt)\n                    .ToArray()\n        }\n\n    type DocumentIdentifier() =\n        member val id = String.Empty with get, set\n        member val PartitionKey = String.Empty with get, set\n\n    type PartitionKeyIdentifier() =\n        member val PartitionKey = String.Empty with get, set\n\n    /// Deletes all documents from CosmosDb.\n    ///\n    /// **** This method is implemented only in Debug configuration. It is a no-op in Release configuration. ****\n    let deleteAllFromCosmosDBThatMatch (queryDefinition: QueryDefinition) =\n        task {\n#if DEBUG\n            let failed = List<string>()\n\n            try\n                let itemRequestOptions =\n                    ItemRequestOptions(AddRequestHeaders = fun headers -> headers.Add(Constants.CorrelationIdHeaderKey, \"deleteAllFromCosmosDBThatMatch\"))\n\n                let mutable totalRecordsDeleted = 0\n                let overallStartTime = getCurrentInstant ()\n                let deleteQueryRequestOptions = queryRequestOptions.ShallowCopy() :?> QueryRequestOptions\n                deleteQueryRequestOptions.MaxItemCount <- 1000\n\n                logToConsole\n                    $\"cosmosContainer.Id: {cosmosContainer.Id}; cosmosContainer.Database.Id: {cosmosContainer.Database.Id}; cosmosContainer.Database.Client.Endpoint: {cosmosContainer.Database.Client.Endpoint}.\"\n\n                let iterator = cosmosContainer.GetItemQueryIterator<PartitionKeyIdentifier>(queryDefinition, requestOptions = deleteQueryRequestOptions)\n\n                while iterator.HasMoreResults do\n                    let batchStartTime = getCurrentInstant ()\n                    let! batchResults = iterator.ReadNextAsync()\n                    let mutable totalRequestCharge = 0L\n\n                    //logToConsole $\"In Services.deleteAllFromCosmosDB(): Current batch size: {batchResults.Resource.Count()}.\"\n\n                    do!\n                        Parallel.ForEachAsync(\n                            batchResults,\n                            (fun document ct ->\n                                ValueTask(\n                                    task {\n                                        use! deleteResponse =\n                                            cosmosContainer.DeleteAllItemsByPartitionKeyStreamAsync(PartitionKey(document.PartitionKey), itemRequestOptions)\n\n                                        if deleteResponse.IsSuccessStatusCode then\n                                            log.LogInformation(\n                                                \"Succeeded to delete PartitionKey {PartitionKey}. StatusCode: {statusCode}.\",\n                                                document.PartitionKey,\n                                                deleteResponse.StatusCode\n                                            )\n                                        else\n                                            failed.Add(document.PartitionKey)\n\n                                            log.LogError(\n                                                \"Failed to delete PartitionKey {PartitionKey}. StatusCode: {statusCode}; Error: {ErrorMessage}.\",\n                                                document.PartitionKey,\n                                                deleteResponse.StatusCode,\n                                                deleteResponse.ErrorMessage\n                                            )\n\n                                    }\n                                ))\n                        )\n\n                //let duration_s = getCurrentInstant().Minus(batchStartTime).TotalSeconds\n                //let overall_duration_s = getCurrentInstant().Minus(overallStartTime).TotalSeconds\n                //let rps = float (batchResults.Resource.Count()) / duration_s\n                //totalRecordsDeleted <- totalRecordsDeleted + batchResults.Resource.Count()\n                //let overallRps = float totalRecordsDeleted / overall_duration_s\n\n                //logToConsole\n                //    $\"In Services.deleteAllFromCosmosDBThatMatch(): batch duration (s): {duration_s:F3}; batch requests/second: {rps:F3}; failed.Count: {failed.Count}; totalRequestCharge: {float totalRequestCharge / 1000.0:F2}; totalRecordsDeleted: {totalRecordsDeleted}; overall duration (m): {overall_duration_s / 60.0:F3}; overall requests/second: {overallRps:F3}.\"\n\n                return failed\n            with\n            | ex ->\n                failed.Add((ExceptionResponse.Create ex).``exception``)\n                return failed\n#else\n            return List<string>([ \"Not implemented\" ])\n#endif\n        }\n\n    /// Deletes all documents from CosmosDB.\n    ///\n    /// **** This method is implemented only in Debug configuration. It is a no-op in Release configuration. ****\n    let deleteAllFromCosmosDb () =\n        task {\n#if DEBUG\n            let queryDefinition = QueryDefinition(\"\"\"SELECT DISTINCT c.PartitionKey FROM c ORDER BY c.PartitionKey\"\"\")\n            return! deleteAllFromCosmosDBThatMatch queryDefinition\n#else\n            return List<string>([ \"Not implemented\" ])\n#endif\n        }\n\n    /// Deletes all Reminders from CosmosDB.\n    ///\n    /// **** This method is implemented only in Debug configuration. It is a no-op in Release configuration. ****\n    let deleteAllRemindersFromCosmosDb () =\n        task {\n#if DEBUG\n            let queryDefinition = QueryDefinition(\"\"\"SELECT c.id, c.PartitionKey FROM c WHERE c.GrainType = \"Rmd\" ORDER BY c.PartitionKey\"\"\")\n            return! deleteAllFromCosmosDBThatMatch queryDefinition\n#else\n            return List<string>([ \"Not implemented\" ])\n#endif\n        }\n\n    /// Gets a list of references of a given ReferenceType for a branch.\n    let getReferencesByType (referenceType: ReferenceType) (repositoryId: RepositoryId) (branchId: BranchId) (maxCount: int) (correlationId: CorrelationId) =\n        task {\n            let references = List<ReferenceDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP @maxCount c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND STRINGEQUALS(c.State[0].Event.created.ReferenceType, @referenceType, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@branchId\", branchId)\n                            .WithParameter(\"@referenceType\", getDiscriminatedUnionCaseName referenceType)\n                            .WithParameter(\"@grainType\", StateName.Reference)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        addTiming TimingFlag.BeforeStorageQuery \"getReferencesByType\" correlationId\n                        let! results = iterator.ReadNextAsync()\n                        addTiming TimingFlag.AfterStorageQuery \"getReferencesByType\" correlationId\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllReferences = results.Resource\n\n                        eventsForAllReferences\n                        |> Seq.iter (fun eventsForOneReference ->\n                            let referenceDto =\n                                eventsForOneReference.State\n                                |> Array.fold\n                                    (fun referenceDto referenceEvent ->\n                                        referenceDto\n                                        |> ReferenceDto.UpdateDto referenceEvent)\n                                    ReferenceDto.Default\n\n                            references.Add(referenceDto))\n\n                    if indexMetrics.Length >= 2\n                       && requestCharge.Length >= 2\n                       && Activity.Current <> null then\n                        Activity\n                            .Current\n                            .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                            .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            return\n                references\n                    .OrderBy(fun reference -> reference.CreatedAt)\n                    .ToArray()\n        }\n\n    let getPromotions = getReferencesByType ReferenceType.Promotion\n    let getCommits = getReferencesByType ReferenceType.Commit\n    let getCheckpoints = getReferencesByType ReferenceType.Checkpoint\n    let getSaves = getReferencesByType ReferenceType.Save\n    let getTags = getReferencesByType ReferenceType.Tag\n    let getExternals = getReferencesByType ReferenceType.External\n    let getRebases = getReferencesByType ReferenceType.Rebase\n\n    let getLatestReference repositoryId branchId =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return None\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let mutable latestReference: ReferenceDto option = None\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@branchId\", branchId)\n                            .WithParameter(\"@grainType\", StateName.Reference)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults && latestReference.IsNone do\n                        let! results = iterator.ReadNextAsync()\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllReferences = results.Resource |> Seq.toArray\n\n                        let mutable index = 0\n\n                        while index < eventsForAllReferences.Length\n                              && latestReference.IsNone do\n                            let referenceDto =\n                                eventsForAllReferences[index].State\n                                |> Array.fold (fun current referenceEvent -> current |> ReferenceDto.UpdateDto referenceEvent) ReferenceDto.Default\n\n                            if isNotDeletedReference referenceDto then latestReference <- Some referenceDto\n\n                            index <- index + 1\n\n                    if (indexMetrics.Length >= 2)\n                       && (requestCharge.Length >= 2)\n                       && Activity.Current <> null then\n                        Activity\n                            .Current\n                            .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                            .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n\n                    return latestReference\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> return None\n        }\n\n    /// Gets the latest reference for a given ReferenceType in a branch.\n    let getLatestReferenceByReferenceTypes (referenceTypes: ReferenceType array) (repositoryId: RepositoryId) (branchId: BranchId) =\n        task {\n            let referenceDtos = ConcurrentDictionary<ReferenceType, ReferenceDto>(referenceTypes.Length, referenceTypes.Length)\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                // Collect per-task metrics in thread-safe bags to avoid concurrent mutation of a single StringBuilder.\n                let indexMetricsBag = ConcurrentBag<string>()\n                let requestChargeBag = ConcurrentBag<string>()\n\n                try\n                    // Run queries in parallel; each task adds metrics to the concurrent bags.\n                    do!\n                        Parallel.ForEachAsync(\n                            referenceTypes,\n                            Constants.ParallelOptions,\n                            (fun referenceType ct ->\n                                ValueTask(\n                                    task {\n                                        let queryDefinition =\n                                            QueryDefinition(\n                                                \"\"\"\n                                                SELECT c.State\n                                                FROM c\n                                                WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                                    AND STRINGEQUALS(c.State[0].Event.created.ReferenceType, @referenceType, true)\n                                                    AND c.GrainType = @grainType\n                                                    AND c.PartitionKey = @partitionKey\n                                                ORDER BY c.State[0].Event.created.CreatedAt DESC\n                                                \"\"\"\n                                            )\n                                                .WithParameter(\"@branchId\", branchId)\n                                                .WithParameter(\"@referenceType\", getDiscriminatedUnionCaseName referenceType)\n                                                .WithParameter(\"@grainType\", StateName.Reference)\n                                                .WithParameter(\"@partitionKey\", repositoryId)\n\n                                        let iterator =\n                                            cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                                        let mutable foundForType = false\n\n                                        while iterator.HasMoreResults && not foundForType do\n                                            let! results = iterator.ReadNextAsync()\n                                            // Save metrics into concurrent bags (thread-safe).\n                                            indexMetricsBag.Add(results.IndexMetrics)\n                                            requestChargeBag.Add($\"{results.RequestCharge:F3}\")\n\n                                            let eventsForAllReferences = results.Resource |> Seq.toArray\n\n                                            let mutable index = 0\n\n                                            while index < eventsForAllReferences.Length\n                                                  && not foundForType do\n                                                let referenceDto =\n                                                    eventsForAllReferences[index].State\n                                                    |> Array.fold\n                                                        (fun current referenceEvent -> current |> ReferenceDto.UpdateDto referenceEvent)\n                                                        ReferenceDto.Default\n\n                                                if isNotDeletedReference referenceDto then\n                                                    referenceDtos.TryAdd(referenceType, referenceDto)\n                                                    |> ignore\n\n                                                    foundForType <- true\n\n                                                index <- index + 1\n                                    }\n                                ))\n                        )\n\n                    // Merge collected metrics after parallel work and set Activity tags.\n                    let indexMetricsSb = stringBuilderPool.Get()\n                    let requestChargeSb = stringBuilderPool.Get()\n\n                    try\n                        for m in indexMetricsBag do\n                            indexMetricsSb.Append($\"{m}, \") |> ignore\n\n                        for r in requestChargeBag do\n                            requestChargeSb.Append($\"{r}, \") |> ignore\n\n                        if (indexMetricsSb.Length >= 2)\n                           && (requestChargeSb.Length >= 2)\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetricsSb.Remove(indexMetricsSb.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestChargeSb.Remove(requestChargeSb.Length - 2, 2)}\")\n                            |> ignore\n                    finally\n                        stringBuilderPool.Return(indexMetricsSb)\n                        stringBuilderPool.Return(requestChargeSb)\n                finally\n                    ()\n            | MongoDB -> ()\n\n            return referenceDtos :> IReadOnlyDictionary<ReferenceType, ReferenceDto>\n        }\n\n    /// Gets the latest reference for a given ReferenceType in a branch.\n    let getLatestReferenceByType (referenceType: ReferenceType) (repositoryId: RepositoryId) (branchId: BranchId) =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return None\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let mutable latestReference: ReferenceDto option = None\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND STRINGEQUALS(c.State[0].Event.created.ReferenceType, @referenceType, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@branchId\", branchId)\n                            .WithParameter(\"@referenceType\", getDiscriminatedUnionCaseName referenceType)\n                            .WithParameter(\"@grainType\", StateName.Reference)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults && latestReference.IsNone do\n                        let! results = DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> iterator.ReadNextAsync())\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllReferences = results.Resource |> Seq.toArray\n\n                        let mutable index = 0\n\n                        while index < eventsForAllReferences.Length\n                              && latestReference.IsNone do\n                            let referenceDto =\n                                eventsForAllReferences[index].State\n                                |> Array.fold (fun current referenceEvent -> current |> ReferenceDto.UpdateDto referenceEvent) ReferenceDto.Default\n\n                            if isNotDeletedReference referenceDto then latestReference <- Some referenceDto\n\n                            index <- index + 1\n\n                    if (indexMetrics.Length >= 2)\n                       && (requestCharge.Length >= 2)\n                       && Activity.Current <> null then\n                        Activity\n                            .Current\n                            .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                            .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n\n                    return latestReference\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> return None\n        }\n\n    /// Gets the latest promotion from a branch.\n    let getLatestPromotion (repositoryId: RepositoryId) (branchId: BranchId) =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return None\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let mutable latestPromotion: ReferenceDto option = None\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND STRINGEQUALS(c.State[0].Event.created.ReferenceType, @referenceType, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@branchId\", branchId)\n                            .WithParameter(\"@referenceType\", getDiscriminatedUnionCaseName ReferenceType.Promotion)\n                            .WithParameter(\"@grainType\", StateName.Reference)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults && latestPromotion.IsNone do\n                        let! results = DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> iterator.ReadNextAsync())\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllReferences = results.Resource |> Seq.toArray\n\n                        let mutable index = 0\n\n                        while index < eventsForAllReferences.Length\n                              && latestPromotion.IsNone do\n                            let referenceDto =\n                                eventsForAllReferences[index].State\n                                |> Array.fold (fun current referenceEvent -> current |> ReferenceDto.UpdateDto referenceEvent) ReferenceDto.Default\n\n                            if isNotDeletedReference referenceDto\n                               && hasPromotionSetTerminalLink referenceDto then\n                                latestPromotion <- Some referenceDto\n\n                            index <- index + 1\n\n                    if (indexMetrics.Length >= 2)\n                       && (requestCharge.Length >= 2)\n                       && Activity.Current <> null then\n                        Activity\n                            .Current\n                            .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                            .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n\n                    return latestPromotion\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> return None\n        }\n\n    /// Gets the latest commit from a branch.\n    let getLatestCommit = getLatestReferenceByType ReferenceType.Commit\n\n    /// Gets the latest checkpoint from a branch.\n    let getLatestCheckpoint = getLatestReferenceByType ReferenceType.Checkpoint\n\n    /// Gets the latest save from a branch.\n    let getLatestSave = getLatestReferenceByType ReferenceType.Save\n\n    /// Gets the latest tag from a branch.\n    let getLatestTag = getLatestReferenceByType ReferenceType.Tag\n\n    /// Gets the latest external from a branch.\n    let getLatestExternal = getLatestReferenceByType ReferenceType.External\n\n    /// Gets the latest rebase from a branch.\n    let getLatestRebase = getLatestReferenceByType ReferenceType.Rebase\n\n    /// Gets a list of branches for a given repository.\n    let getBranches (ownerId: OwnerId) (organizationId: OrganizationId) (repositoryId: RepositoryId) (maxCount: int) includeDeleted correlationId =\n        task {\n            let branches = ConcurrentDictionary<BranchId, BranchDto>()\n            let branchIds = List<BranchId>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    try\n                        // First, get all of the branches for the repository.\n                        let queryDefinition =\n                            QueryDefinition(\n                                $\"\"\"\n                                SELECT TOP @maxCount c.State[0].Event.created.branchId\n                                FROM c\n                                WHERE STRINGEQUALS(c.State[0].Event.created.ownerId, @ownerId, true)\n                                    AND STRINGEQUALS(c.State[0].Event.created.organizationId, @organizationId, true)\n                                    AND LENGTH(c.State[0].Event.created.branchName) > 0\n                                    {includeDeletedEntitiesClause includeDeleted}\n                                    AND c.GrainType = @grainType\n                                    AND c.PartitionKey = @partitionKey\n                                \"\"\"\n                            )\n                                .WithParameter(\"@maxCount\", maxCount)\n                                .WithParameter(\"@ownerId\", ownerId)\n                                .WithParameter(\"@organizationId\", organizationId)\n                                .WithParameter(\"@grainType\", StateName.Branch)\n                                .WithParameter(\"@partitionKey\", repositoryId)\n\n                        let iterator = cosmosContainer.GetItemQueryIterator<BranchIdValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                        while iterator.HasMoreResults do\n                            addTiming TimingFlag.BeforeStorageQuery \"getBranches\" correlationId\n                            let! results = iterator.ReadNextAsync()\n                            addTiming TimingFlag.AfterStorageQuery \"getBranches\" correlationId\n\n                            indexMetrics.Append($\"{results.IndexMetrics}, \")\n                            |> ignore\n\n                            requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                            |> ignore\n\n                            let branchIdValues = results.Resource\n\n                            for branchIdValue in branchIdValues do\n                                branchIds.Add(branchIdValue.branchId)\n\n                        do!\n                            Parallel.ForEachAsync(\n                                branchIds,\n                                (fun branchId ct ->\n                                    ValueTask(\n                                        task {\n                                            let actorProxy = Branch.CreateActorProxy branchId repositoryId correlationId\n                                            let! branchDto = actorProxy.Get correlationId\n                                            branches[branchDto.BranchId] <- branchDto\n                                        }\n                                    ))\n                            )\n\n                        if indexMetrics.Length >= 2\n                           && requestCharge.Length >= 2\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                            |> ignore\n                    with\n                    | ex ->\n                        logToConsole $\"Got an exception.\"\n                        logToConsole $\"{ExceptionResponse.Create ex}\"\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            return\n                branches\n                    .Values\n                    .OrderBy(fun branchDto -> branchDto.UpdatedAt)\n                    .ToArray()\n        }\n\n    /// Gets a DirectoryVersion by searching using a Sha256Hash value.\n    let getDirectoryVersionBySha256Hash (repositoryId: RepositoryId) (sha256Hash: Sha256Hash) correlationId =\n        task {\n            let mutable directoryVersion = DirectoryVersion.Default\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP 1 c.State\n                            FROM c\n                            WHERE STARTSWITH(c.State[0].Event.created.Sha256Hash, @sha256Hash, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@sha256Hash\", sha256Hash)\n                            .WithParameter(\"@grainType\", StateName.DirectoryVersion)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    try\n                        let iterator = cosmosContainer.GetItemQueryIterator<DirectoryVersionEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                        let directoryVersionDtos = List<DirectoryVersionDto>()\n\n                        while iterator.HasMoreResults do\n                            let! results = DefaultAsyncRetryPolicy.ExecuteAsync(fun () -> iterator.ReadNextAsync())\n\n                            indexMetrics.Append($\"{results.IndexMetrics}, \")\n                            |> ignore\n\n                            requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                            |> ignore\n\n                            let eventsForAllDirectories = results.Resource\n\n                            eventsForAllDirectories\n                            |> Seq.iter (fun eventsForOneDirectory ->\n                                let directoryVersionDto =\n                                    eventsForOneDirectory.State\n                                    |> Array.fold\n                                        (fun directoryVersionDto directoryEvent ->\n                                            directoryVersionDto\n                                            |> DirectoryVersionDto.UpdateDto directoryEvent)\n                                        DirectoryVersionDto.Default\n\n                                directoryVersionDtos.Add(directoryVersionDto))\n\n                            if directoryVersionDtos.Count > 0 then\n                                directoryVersion <- directoryVersionDtos[0].DirectoryVersion\n\n                        if (indexMetrics.Length >= 2)\n                           && (requestCharge.Length >= 2)\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                            |> ignore\n                    with\n                    | ex ->\n                        log.LogError(\n                            ex,\n                            \"{CurrentInstant}: Exception in Services.getDirectoryBySha256Hash(). QueryDefinition: {queryDefinition}\",\n                            getCurrentInstantExtended (),\n                            (serialize queryDefinition)\n                        )\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            if directoryVersion.DirectoryVersionId\n               <> DirectoryVersion.Default.DirectoryVersionId then\n                return Some directoryVersion\n            else\n                return None\n        }\n\n    /// Gets the most recent DirectoryVersion with HashesValidated = true by RelativePath.\n    let getMostRecentDirectoryVersionByRelativePath (repositoryId: RepositoryId) (relativePath: RelativePath) correlationId =\n        task {\n            let mutable directoryVersion = DirectoryVersion.Default\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP 1 c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.RelativePath, @relativePath, true)\n                                AND c.State[0].Event.created.HashesValidated = true\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@relativePath\", relativePath)\n                            .WithParameter(\"@grainType\", StateName.DirectoryVersion)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<DirectoryVersionEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync()\n\n                        indexMetrics.Append($\"{results.IndexMetrics}, \")\n                        |> ignore\n\n                        requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                        |> ignore\n\n                        let eventsForAllDirectories = results.Resource\n\n                        eventsForAllDirectories\n                        |> Seq.iter (fun eventsForOneDirectory ->\n                            let directoryVersionDto =\n                                eventsForOneDirectory.State\n                                |> Array.fold\n                                    (fun directoryVersionDto directoryEvent ->\n                                        directoryVersionDto\n                                        |> DirectoryVersionDto.UpdateDto directoryEvent)\n                                    DirectoryVersionDto.Default\n\n                            directoryVersion <- directoryVersionDto.DirectoryVersion)\n\n                    if (indexMetrics.Length >= 2)\n                       && (requestCharge.Length >= 2)\n                       && Activity.Current <> null then\n                        Activity\n                            .Current\n                            .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                            .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                        |> ignore\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            if directoryVersion.DirectoryVersionId\n               <> DirectoryVersion.Default.DirectoryVersionId then\n                return Some directoryVersion\n            else\n                return None\n        }\n\n    /// Gets a Root DirectoryVersion by searching using a Sha256Hash value.\n    let getRootDirectoryVersionBySha256Hash (repositoryId: RepositoryId) (sha256Hash: Sha256Hash) correlationId =\n        task {\n            let mutable directoryVersion = DirectoryVersion.Default\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            $\"\"\"\n                            SELECT TOP 1 c.State\n                            FROM c\n                            WHERE STARTSWITH(c.State[0].Event.created.Sha256Hash, @sha256Hash, true)\n                                AND STRINGEQUALS(c.State[0].Event.created.RelativePath, @relativePath, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            ORDER BY c.State[0].Event.created.CreatedAt DESC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@sha256Hash\", sha256Hash)\n                            .WithParameter(\"@relativePath\", Constants.RootDirectoryPath)\n                            .WithParameter(\"@grainType\", StateName.DirectoryVersion)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    try\n                        let iterator = cosmosContainer.GetItemQueryIterator<DirectoryVersionEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n                        let directoryVersionDtos = List<DirectoryVersionDto>()\n\n                        while iterator.HasMoreResults do\n                            let! results = iterator.ReadNextAsync()\n\n                            indexMetrics.Append($\"{results.IndexMetrics}, \")\n                            |> ignore\n\n                            requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                            |> ignore\n\n                            let eventsForAllDirectories = results.Resource\n\n                            eventsForAllDirectories\n                            |> Seq.iter (fun eventsForOneDirectory ->\n                                let directoryVersionDto =\n                                    eventsForOneDirectory.State\n                                    |> Array.fold\n                                        (fun directoryVersionDto directoryEvent ->\n                                            directoryVersionDto\n                                            |> DirectoryVersionDto.UpdateDto directoryEvent)\n                                        DirectoryVersionDto.Default\n\n                                directoryVersionDtos.Add(directoryVersionDto))\n\n                        if directoryVersionDtos.Count > 0 then\n                            directoryVersion <- directoryVersionDtos[0].DirectoryVersion\n\n                        if (indexMetrics.Length >= 2)\n                           && (requestCharge.Length >= 2)\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                            |> ignore\n                    with\n                    | ex ->\n                        let parameters =\n                            queryDefinition.GetQueryParameters()\n                            |> Seq.fold (fun (state: StringBuilder) (struct (k, v)) -> state.Append($\"{k} = {v}; \")) (StringBuilder())\n\n                        log.LogError(\n                            ex,\n                            \"{CurrentInstant}: Exception in Services.getRootDirectoryBySha256Hash(). QueryText: {queryText}. Parameters: {parameters}\",\n                            getCurrentInstantExtended (),\n                            (queryDefinition.QueryText),\n                            parameters.ToString()\n                        )\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            if directoryVersion.DirectoryVersionId\n               <> DirectoryVersion.Default.DirectoryVersionId then\n                return Some directoryVersion\n            else\n                return None\n        }\n\n    /// Gets a Root DirectoryVersion by searching using a Sha256Hash value.\n    let getRootDirectoryVersionByReferenceId (repositoryId: RepositoryId) (referenceId: ReferenceId) correlationId =\n        task {\n            let referenceActorProxy = Reference.CreateActorProxy referenceId repositoryId correlationId\n\n            let! referenceDto = referenceActorProxy.Get correlationId\n\n            return! getRootDirectoryVersionBySha256Hash repositoryId referenceDto.Sha256Hash correlationId\n        }\n\n    /// Checks if all of the supplied DirectoryVersionIds exist.\n    let directoryVersionIdsExist (repositoryId: RepositoryId) (directoryVersionIds: IEnumerable<DirectoryVersionId>) correlationId =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return false\n            | AzureCosmosDb ->\n                let mutable requestCharge = 0.0\n                let mutable allExist = true\n                let directoryVersionIdQueue = Queue<DirectoryVersionId>(directoryVersionIds)\n\n                while directoryVersionIdQueue.Count > 0 && allExist do\n                    let directoryVersionId = directoryVersionIdQueue.Dequeue()\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.DirectoryVersionId, @directoryVersionId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                            .WithParameter(\"@directoryVersionId\", $\"{directoryVersionId}\")\n                            .WithParameter(\"@grainType\", StateName.DirectoryVersion)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<DirectoryVersionEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync()\n                        requestCharge <- requestCharge + results.RequestCharge\n\n                        if not <| results.Resource.Any() then allExist <- false\n\n                Activity\n                    .Current\n                    .SetTag(\"allExist\", $\"{allExist}\")\n                    .SetTag(\"totalRequestCharge\", $\"{requestCharge}\")\n                |> ignore\n\n                return allExist\n            | MongoDB -> return false\n        }\n\n    /// Gets a list of ReferenceDtos based on ReferenceIds. The list is returned in the same order as the supplied ReferenceIds.\n    let getReferencesByReferenceId (repositoryId: RepositoryId) (referenceIds: IEnumerable<ReferenceId>) (maxCount: int) (correlationId: CorrelationId) =\n        task {\n            let referenceDtos = List<ReferenceDto>()\n\n            if referenceIds.Count() > 0 then\n                match actorStateStorageProvider with\n                | Unknown -> ()\n                | AzureCosmosDb ->\n                    let mutable requestCharge = 0.0\n                    let mutable clientElapsedTime = TimeSpan.Zero\n                    let queryText = stringBuilderPool.Get()\n\n                    try\n                        // In order to build the IN clause, we need to create a parameter for each referenceId.\n                        //   (I tried just using string concatenation, it didn't work for some reason. Anyway...)\n                        // The query starts with:\n                        queryText.Append(\n                            @\"SELECT TOP @maxCount c.State\n                              FROM c\n                              WHERE c.GrainType = @grainType\n                                  AND c.PartitionKey = @partitionKey\n                                  AND c.State[0].Event.created.ReferenceId IN (\"\n                        )\n                        |> ignore\n                        // Then we add a parameter for each referenceId.\n                        referenceIds\n                            .Where(fun referenceId -> not <| referenceId.Equals(ReferenceId.Empty))\n                            .Distinct()\n                        |> Seq.iteri (fun i referenceId -> queryText.Append($\"@referenceId{i},\") |> ignore)\n                        // Then we remove the last comma and close the parenthesis.\n                        queryText\n                            .Remove(queryText.Length - 1, 1)\n                            .Append(\")\")\n                        |> ignore\n\n                        // Create the query definition.\n                        let queryDefinition =\n                            QueryDefinition(queryText.ToString())\n                                .WithParameter(\"@maxCount\", referenceIds.Count())\n                                .WithParameter(\"@grainType\", StateName.Reference)\n                                .WithParameter(\"@partitionKey\", repositoryId)\n\n                        // Add a .WithParameter for each referenceId.\n                        referenceIds\n                            .Where(fun referenceId -> not <| referenceId.Equals(ReferenceId.Empty))\n                            .Distinct()\n                        |> Seq.iteri (fun i referenceId ->\n                            queryDefinition.WithParameter($\"@referenceId{i}\", $\"{referenceId}\")\n                            |> ignore)\n\n                        //logToConsole $\"In getReferencesByReferenceId(): QueryText:{Environment.NewLine}{printQueryDefinition queryDefinition}.\"\n\n                        // Execute the query.\n                        let iterator = cosmosContainer.GetItemQueryIterator<ReferenceEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                        // The query will return fewer results than the number of referenceIds if the supplied referenceIds have duplicates.\n                        //   This is normal for `grace status` (BasedOn and Latest Promotion are likely to be the same, for instance).\n                        //   We need to gather the query results, and then iterate through the referenceId's to return the dto's in the same order.\n                        let queryResults = Dictionary<ReferenceId, ReferenceDto>()\n\n                        while iterator.HasMoreResults do\n                            addTiming TimingFlag.BeforeStorageQuery \"getReferencesByReferenceId\" correlationId\n                            let! results = iterator.ReadNextAsync()\n                            addTiming TimingFlag.AfterStorageQuery \"getReferencesByReferenceId\" correlationId\n                            requestCharge <- requestCharge + results.RequestCharge\n\n                            clientElapsedTime <-\n                                clientElapsedTime\n                                + results.Diagnostics.GetClientElapsedTime()\n\n                            let eventsForAllReferences = results.Resource\n\n                            eventsForAllReferences\n                            |> Seq.iter (fun eventsForOneReference ->\n                                let referenceDto =\n                                    eventsForOneReference.State\n                                    |> Array.fold\n                                        (fun referenceDto referenceEvent ->\n                                            referenceDto\n                                            |> ReferenceDto.UpdateDto referenceEvent)\n                                        ReferenceDto.Default\n\n                                queryResults.Add(referenceDto.ReferenceId, referenceDto))\n\n                        // Add the results to the list in the same order as the supplied referenceIds.\n                        referenceIds\n                        |> Seq.iter (fun referenceId ->\n                            if referenceId <> ReferenceId.Empty then\n                                if queryResults.ContainsKey(referenceId) then\n                                    referenceDtos.Add(queryResults[referenceId])\n                            else\n                                // In case the caller supplied an empty referenceId, add a default ReferenceDto.\n                                referenceDtos.Add(ReferenceDto.Default))\n\n                        Activity\n                            .Current\n                            .SetTag(\"referenceDtos.Count\", $\"{referenceDtos.Count}\")\n                            .SetTag(\"clientElapsedTime\", $\"{clientElapsedTime}\")\n                            .SetTag(\"totalRequestCharge\", $\"{requestCharge}\")\n                        |> ignore\n                    finally\n                        stringBuilderPool.Return(queryText)\n                | MongoDB -> ()\n\n            return referenceDtos\n        }\n\n    /// Gets a list of BranchDtos based on BranchIds.\n    let getBranchesByBranchId (repositoryId: RepositoryId) (branchIds: IEnumerable<BranchId>) (maxCount: int) includeDeleted =\n        task {\n            let branchDtos = List<BranchDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let mutable requestCharge = 0.0\n\n                let branchIdStack = Queue<ReferenceId>(branchIds)\n\n                while branchIdStack.Count > 0 do\n                    let branchId = branchIdStack.Dequeue()\n\n                    let queryDefinition =\n                        QueryDefinition(\n                            $\"\"\"\n                            SELECT TOP @maxCount c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.BranchId, @branchId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                                {includeDeletedEntitiesClause includeDeleted}\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@branchId\", $\"{branchId}\")\n                            .WithParameter(\"@grainType\", StateName.Branch)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<BranchEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync()\n                        requestCharge <- requestCharge + results.RequestCharge\n                        let eventsForAllBranches = results.Resource\n\n                        eventsForAllBranches\n                        |> Seq.iter (fun eventsForOneBranch ->\n                            let branchDto =\n                                eventsForOneBranch.State\n                                |> Array.fold (fun branchDto branchEvent -> branchDto |> BranchDto.UpdateDto branchEvent) BranchDto.Default\n\n                            branchDtos.Add(branchDto))\n\n                if Activity.Current <> null then\n                    Activity\n                        .Current\n                        .SetTag(\"referenceDtos.Count\", $\"{branchDtos.Count}\")\n                        .SetTag(\"totalRequestCharge\", $\"{requestCharge}\")\n                    |> ignore\n            | MongoDB -> ()\n\n            return\n                branchDtos\n                    .OrderBy(fun branchDto -> branchDto.BranchName)\n                    .ToArray()\n        }\n\n    /// Gets a list of child BranchDtos for a given parent branch.\n    let getChildBranches (repositoryId: RepositoryId) (parentBranchId: BranchId) (maxCount: int) includeDeleted correlationId =\n        task {\n            let childBranches = List<BranchDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let mutable requestCharge = 0.0\n\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            $\"\"\"\n                            SELECT TOP @maxCount c.State\n                            FROM c\n                            WHERE STRINGEQUALS(c.State[0].Event.created.parentBranchId, @parentBranchId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                                {includeDeletedEntitiesClause includeDeleted}\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@parentBranchId\", $\"{parentBranchId}\")\n                            .WithParameter(\"@grainType\", StateName.Branch)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<BranchEventValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        addTiming TimingFlag.BeforeStorageQuery \"getChildBranches\" correlationId\n                        let! results = iterator.ReadNextAsync()\n                        addTiming TimingFlag.AfterStorageQuery \"getChildBranches\" correlationId\n                        requestCharge <- requestCharge + results.RequestCharge\n                        let eventsForAllBranches = results.Resource\n\n                        eventsForAllBranches\n                        |> Seq.iter (fun eventsForOneBranch ->\n                            let branchDto =\n                                eventsForOneBranch.State\n                                |> Array.fold (fun branchDto branchEvent -> branchDto |> BranchDto.UpdateDto branchEvent) BranchDto.Default\n\n                            childBranches.Add(branchDto))\n\n                    if (Activity.Current <> null) then\n                        Activity\n                            .Current\n                            .SetTag(\"childBranches.Count\", $\"{childBranches.Count}\")\n                            .SetTag(\"totalRequestCharge\", $\"{requestCharge}\")\n                        |> ignore\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Error in getChildBranches. CorrelationId: {correlationId}.\",\n                        getCurrentInstantExtended (),\n                        correlationId\n                    )\n            | MongoDB -> ()\n\n            return\n                childBranches\n                    .OrderBy(fun branchDto -> branchDto.BranchName)\n                    .ToArray()\n        }\n\n    /// Gets validation sets for a repository.\n    let getValidationSets (repositoryId: RepositoryId) (maxCount: int) includeDeleted correlationId =\n        task {\n            let validationSets = ConcurrentBag<ValidationSetDto>()\n            let validationSetIds = List<ValidationSetId>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP @maxCount c.id\n                            FROM c\n                            WHERE c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@grainType\", StateName.ValidationSet)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ActorIdValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync()\n\n                        results.Resource\n                        |> Seq.iter (fun value ->\n                            let mutable parsed = Guid.Empty\n\n                            if String.IsNullOrWhiteSpace value.id |> not\n                               && Guid.TryParse(value.id, &parsed)\n                               && parsed <> Guid.Empty then\n                                validationSetIds.Add(parsed))\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Error in getValidationSets. CorrelationId: {correlationId}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        correlationId,\n                        repositoryId\n                    )\n            | MongoDB -> ()\n\n            do!\n                Parallel.ForEachAsync(\n                    validationSetIds,\n                    (fun validationSetId ct ->\n                        ValueTask(\n                            task {\n                                let actorProxy = ValidationSet.CreateActorProxy validationSetId repositoryId correlationId\n                                let! validationSet = actorProxy.Get correlationId\n\n                                match validationSet with\n                                | Some dto when includeDeleted || dto.DeletedAt.IsNone -> validationSets.Add(dto)\n                                | _ -> ()\n                            }\n                        ))\n                )\n\n            return\n                validationSets\n                |> Seq.sortByDescending (fun dto -> dto.CreatedAt)\n                |> Seq.toList\n        }\n\n    /// Gets validation results for a PromotionSet and computation attempt.\n    let getValidationResultsForPromotionSetAttempt\n        (repositoryId: RepositoryId)\n        (promotionSetId: PromotionSetId)\n        (stepsComputationAttempt: int)\n        (maxCount: int)\n        correlationId\n        =\n        task {\n            let validationResults = ConcurrentBag<ValidationResultDto>()\n            let validationResultIds = List<ValidationResultId>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP @maxCount c.id\n                            FROM c\n                            WHERE c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", maxCount)\n                            .WithParameter(\"@grainType\", StateName.ValidationResult)\n                            .WithParameter(\"@partitionKey\", repositoryId)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ActorIdValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync()\n\n                        results.Resource\n                        |> Seq.iter (fun value ->\n                            let mutable parsed = Guid.Empty\n\n                            if String.IsNullOrWhiteSpace value.id |> not\n                               && Guid.TryParse(value.id, &parsed)\n                               && parsed <> Guid.Empty then\n                                validationResultIds.Add(parsed))\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Error in getValidationResultsForPromotionSetAttempt. CorrelationId: {correlationId}; RepositoryId: {repositoryId}; PromotionSetId: {promotionSetId}.\",\n                        getCurrentInstantExtended (),\n                        correlationId,\n                        repositoryId,\n                        promotionSetId\n                    )\n            | MongoDB -> ()\n\n            do!\n                Parallel.ForEachAsync(\n                    validationResultIds,\n                    (fun validationResultId ct ->\n                        ValueTask(\n                            task {\n                                let actorProxy = ValidationResult.CreateActorProxy validationResultId repositoryId correlationId\n                                let! validationResult = actorProxy.Get correlationId\n\n                                match validationResult with\n                                | Some dto when\n                                    dto.PromotionSetId = Some promotionSetId\n                                    && dto.StepsComputationAttempt = Some stepsComputationAttempt\n                                    ->\n                                    validationResults.Add(dto)\n                                | _ -> ()\n                            }\n                        ))\n                )\n\n            return\n                validationResults\n                |> Seq.sortByDescending (fun dto -> dto.CreatedAt)\n                |> Seq.toList\n        }\n\n    let getWorkItemIdByNumber (repositoryId: RepositoryId) (workItemNumber: WorkItemNumber) (correlationId: CorrelationId) =\n        task {\n            match actorStateStorageProvider with\n            | Unknown -> return None\n            | AzureCosmosDb ->\n                try\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP 1 c.id\n                            FROM c\n                            WHERE c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                                AND c.State[0].Event.created.workItemNumber = @workItemNumber\n                            \"\"\"\n                        )\n                            .WithParameter(\"@grainType\", StateName.WorkItem)\n                            .WithParameter(\"@partitionKey\", $\"{repositoryId}\")\n                            .WithParameter(\"@workItemNumber\", workItemNumber)\n\n                    let iterator = cosmosContainer.GetItemQueryIterator<ActorIdValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                    if iterator.HasMoreResults then\n                        let! results = iterator.ReadNextAsync()\n                        let actorId =\n                            results.Resource\n                            |> Seq.tryHead\n                            |> Option.map (fun value -> value.id)\n                            |> Option.defaultValue String.Empty\n\n                        if String.IsNullOrWhiteSpace(actorId) then\n                            return None\n                        else\n                            let mutable workItemId = Guid.Empty\n\n                            if\n                                Guid.TryParse(actorId, &workItemId)\n                                && workItemId <> Guid.Empty\n                            then\n                                return Some workItemId\n                            else\n                                return None\n                    else\n                        return None\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Error in getWorkItemIdByNumber. CorrelationId: {correlationId}; RepositoryId: {repositoryId}; WorkItemNumber: {workItemNumber}.\",\n                        getCurrentInstantExtended (),\n                        correlationId,\n                        repositoryId,\n                        workItemNumber\n                    )\n\n                    return None\n            | MongoDB -> return None\n        }\n\n    /// Gets a list of reminders for a repository, with optional filtering.\n    let getReminders\n        (graceIds: GraceIds)\n        (maxCount: int)\n        (reminderTypeFilter: string option)\n        (actorNameFilter: string option)\n        (dueAfter: Instant option)\n        (dueBefore: Instant option)\n        (correlationId: CorrelationId)\n        =\n        task {\n            let reminders = List<ReminderDto>()\n\n            match actorStateStorageProvider with\n            | Unknown -> ()\n            | AzureCosmosDb ->\n                let indexMetrics = stringBuilderPool.Get()\n                let requestCharge = stringBuilderPool.Get()\n\n                try\n                    try\n                        // Build the query dynamically based on provided filters\n                        let queryBuilder = StringBuilder()\n\n                        queryBuilder.Append(\n                            \"\"\"\n                            SELECT TOP @maxCount c.State.Reminder\n                            FROM c\n                            WHERE STRINGEQUALS(c.State.Reminder.OwnerId, @ownerId, true)\n                                AND STRINGEQUALS(c.State.Reminder.OrganizationId, @organizationId, true)\n                                AND STRINGEQUALS(c.State.Reminder.RepositoryId, @repositoryId, true)\n                                AND c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                            \"\"\"\n                        )\n                        |> ignore\n\n                        // Add optional filters\n                        if\n                            reminderTypeFilter.IsSome\n                            && not (String.IsNullOrEmpty(reminderTypeFilter.Value))\n                        then\n                            queryBuilder.Append(\" AND STRINGEQUALS(c.State.Reminder.ReminderType, @reminderType, true)\")\n                            |> ignore\n\n                        if\n                            actorNameFilter.IsSome\n                            && not (String.IsNullOrEmpty(actorNameFilter.Value))\n                        then\n                            queryBuilder.Append(\" AND STRINGEQUALS(c.State.Reminder.ActorName, @actorName, true)\")\n                            |> ignore\n\n                        if dueAfter.IsSome then\n                            queryBuilder.Append(\" AND c.State.Reminder.ReminderTime >= @dueAfter\")\n                            |> ignore\n\n                        if dueBefore.IsSome then\n                            queryBuilder.Append(\" AND c.State.Reminder.ReminderTime <= @dueBefore\")\n                            |> ignore\n\n                        queryBuilder.Append(\" ORDER BY c.State.Reminder.ReminderTime ASC\")\n                        |> ignore\n\n                        let queryDefinition =\n                            QueryDefinition(queryBuilder.ToString())\n                                .WithParameter(\"@maxCount\", maxCount)\n                                .WithParameter(\"@ownerId\", graceIds.OwnerIdString)\n                                .WithParameter(\"@organizationId\", graceIds.OrganizationIdString)\n                                .WithParameter(\"@repositoryId\", graceIds.RepositoryIdString)\n                                .WithParameter(\"@grainType\", StateName.Reminder)\n                                .WithParameter(\"@partitionKey\", StateName.Reminder)\n\n                        // Add optional parameters\n                        if\n                            reminderTypeFilter.IsSome\n                            && not (String.IsNullOrEmpty(reminderTypeFilter.Value))\n                        then\n                            queryDefinition.WithParameter(\"@reminderType\", reminderTypeFilter.Value)\n                            |> ignore\n\n                        if\n                            actorNameFilter.IsSome\n                            && not (String.IsNullOrEmpty(actorNameFilter.Value))\n                        then\n                            queryDefinition.WithParameter(\"@actorName\", actorNameFilter.Value)\n                            |> ignore\n\n                        if dueAfter.IsSome then\n                            queryDefinition.WithParameter(\"@dueAfter\", dueAfter.Value.ToUnixTimeTicks())\n                            |> ignore\n\n                        if dueBefore.IsSome then\n                            queryDefinition.WithParameter(\"@dueBefore\", dueBefore.Value.ToUnixTimeTicks())\n                            |> ignore\n\n                        let iterator = cosmosContainer.GetItemQueryIterator<ReminderValue>(queryDefinition, requestOptions = queryRequestOptions)\n\n                        while iterator.HasMoreResults do\n                            let! results = iterator.ReadNextAsync()\n\n                            indexMetrics.Append($\"{results.IndexMetrics}, \")\n                            |> ignore\n\n                            requestCharge.Append($\"{results.RequestCharge:F3}, \")\n                            |> ignore\n\n                            let reminderValues = results.Resource\n\n                            reminderValues\n                            |> Seq.iter (fun reminderValue -> reminders.Add(reminderValue.Reminder))\n\n                        if (indexMetrics.Length >= 2)\n                           && (requestCharge.Length >= 2)\n                           && Activity.Current <> null then\n                            Activity\n                                .Current\n                                .SetTag(\"indexMetrics\", $\"{indexMetrics.Remove(indexMetrics.Length - 2, 2)}\")\n                                .SetTag(\"requestCharge\", $\"{requestCharge.Remove(requestCharge.Length - 2, 2)}\")\n                            |> ignore\n                    with\n                    | ex ->\n                        log.LogError(\n                            ex,\n                            \"{CurrentInstant}: Error in getReminders. CorrelationId: {correlationId}.\",\n                            getCurrentInstantExtended (),\n                            correlationId\n                        )\n                finally\n                    stringBuilderPool.Return(indexMetrics)\n                    stringBuilderPool.Return(requestCharge)\n            | MongoDB -> ()\n\n            logToConsole $\"Found {reminders.Count} reminders for RepositoryId {graceIds.RepositoryIdString}.\"\n            logToConsole $\"Reminders: {serialize reminders}.\"\n            return reminders.ToArray()\n        }\n\n    /// Gets a single reminder by its ID.\n    let getReminderById (reminderId: ReminderId) (correlationId: CorrelationId) =\n        task {\n            let reminderActorProxy = Reminder.CreateActorProxy reminderId correlationId\n            let! exists = reminderActorProxy.Exists correlationId\n\n            if exists then\n                let! reminderDto = reminderActorProxy.Get correlationId\n                return Some reminderDto\n            else\n                return None\n        }\n\n    /// Deletes a reminder by its ID.\n    let deleteReminder (reminderId: ReminderId) (correlationId: CorrelationId) =\n        task {\n            let reminderActorProxy = Reminder.CreateActorProxy reminderId correlationId\n            let! exists = reminderActorProxy.Exists correlationId\n\n            if exists then\n                do! reminderActorProxy.Delete correlationId\n                return Ok()\n            else\n                return Error \"Reminder not found.\"\n        }\n\n    /// Creates a new reminder actor instance.\n    let createReminder (reminderDto: ReminderDto) =\n        task {\n            let reminderActorProxy = Reminder.CreateActorProxy reminderDto.ReminderId reminderDto.CorrelationId\n            do! reminderActorProxy.Create reminderDto reminderDto.CorrelationId\n        }\n        :> Task\n\n    /// Gets the CorrelationId from an Orleans grain's RequestContext.\n    let getCorrelationId () =\n        match RequestContext.Get(Constants.CorrelationId) with\n        | :? string as s -> s\n        | _ -> String.Empty\n\n    /// Gets the ActorName from an Orleans grain's RequestContext.\n    let getActorName () =\n        match RequestContext.Get(Constants.ActorNameProperty) with\n        | :? string as s -> s\n        | _ -> String.Empty\n\n    /// Gets the CurrentCommand from an Orleans grain's RequestContext.\n    let getCurrentCommand () =\n        match RequestContext.Get(Constants.CurrentCommandProperty) with\n        | :? string as s -> s\n        | _ -> String.Empty\n\n    /// Gets the OrganizationId from an Orleans grain's RequestContext.\n    let getOrganizationId () =\n        match RequestContext.Get(nameof OrganizationId) with\n        | :? OrganizationId as organizationId -> organizationId\n        | _ -> Guid.Empty\n\n    /// Gets the RepositoryId from an Orleans grain's RequestContext.\n    let getRepositoryId () =\n        match RequestContext.Get(nameof RepositoryId) with\n        | :? RepositoryId as repositoryId -> repositoryId\n        | _ -> Guid.Empty\n\n    /// Gets a message that says whether an actor's state was retrieved from the database.\n    let getActorActivationMessage recordExists = if recordExists then \"Retrieved from database\" else \"Not found in database\"\n\n    /// Logs the activation of an actor.\n    let logActorActivation (log: ILogger) (grainIdentity: string) (activationStartTime: Instant) (message: string) =\n        log.LogInformation(\n            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Activated {GrainIdentity}. {message}.\",\n            getCurrentInstantExtended (),\n            getMachineName,\n            (getDurationRightAligned_ms activationStartTime),\n            getCorrelationId (),\n            grainIdentity,\n            message\n        )\n"
  },
  {
    "path": "src/Grace.Actors/Timing.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Context\nopen Grace.Actors.Types\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen System\nopen System.Collections.Generic\nopen System.Linq\nopen System.Reflection\nopen System.Diagnostics\n\nmodule Timing =\n\n    let publishTimings sb =\n        let message = sb.ToString()\n        logToConsole message\n\n    let addTiming flag actorStateName correlationId =\n        //let timingList = timings.GetOrAdd(correlationId, (fun _ -> List<Timing>()))\n        //let timing = Timing.Create flag actorStateName\n        //timingList.Add(timing)\n        ()\n\n    let reportTimings path correlationId =\n        let mutable timingList = null\n        let sb = stringBuilderPool.Get()\n\n        try\n            if timings.TryGetValue(correlationId, &timingList) then\n                match timingList.Count with\n                | 0\n                | 1 -> ()\n                | _ ->\n                    sb\n                        .AppendLine()\n                        .AppendLine(String.replicate 80 \"=\")\n                    |> ignore\n\n                    sb.AppendLine($\"CorrelationId: {correlationId}; Path: {path}; Timings: {timingList.Count} \")\n                    |> ignore\n\n                    sb.AppendLine(String.replicate 80 \"-\") |> ignore\n\n                    sb.AppendLine($\"  {formatInstantExtended timingList[0].Time}: {getDiscriminatedUnionCaseName timingList[0].Flag}\")\n                    |> ignore\n\n                    for i in 1 .. timingList.Count - 1 do\n                        let previousTiming = timingList[i - 1]\n                        let currentTiming = timingList[i]\n                        logToConsole $\"*******In reportTimings: correlationId: {correlationId}; timingList.Count: {timingList.Count}; i: {i}.\"\n\n                        let previousActorStateName =\n                            if String.IsNullOrEmpty(previousTiming.ActorStateName) then\n                                String.Empty\n                            else\n                                \":\" + previousTiming.ActorStateName\n\n                        let currentActorStateName =\n                            if String.IsNullOrEmpty(currentTiming.ActorStateName) then\n                                String.Empty\n                            else\n                                \":\" + currentTiming.ActorStateName\n\n                        let milliseconds =\n                            $\"{(timingList[i].Time - previousTiming.Time)\n                                   .TotalMilliseconds:F3}\"\n\n                        let paddedDuration =\n                            (String.replicate (Math.Max(7 - milliseconds.Length, 0)) \" \")\n                            + milliseconds // Right-align, 7 characters.\n\n                        sb.AppendLine(\n                            $\"  {formatInstantExtended currentTiming.Time}: Duration: {paddedDuration}ms; {getDiscriminatedUnionCaseName previousTiming.Flag}{previousActorStateName} -> {getDiscriminatedUnionCaseName currentTiming.Flag}{currentActorStateName}\"\n                        )\n                        |> ignore\n\n                    let duration =\n                        timingList\n                            .Last()\n                            .Time.Minus(timingList.First().Time)\n\n                    let milliseconds = $\"{duration.TotalMilliseconds:F3}\"\n\n                    let paddedDuration =\n                        (String.replicate (Math.Max(7 - milliseconds.Length, 0)) \" \")\n                        + milliseconds // Right-align, 7 characters.\n\n                    let space = \" \"\n\n                    sb.AppendLine(String.replicate 80 \"-\") |> ignore\n\n                    if duration.TotalMilliseconds > 500.0 then\n                        sb.AppendLine($\"{String.replicate 32 space}Total:    {paddedDuration}ms ##########\")\n                    else\n                        sb.AppendLine($\"{String.replicate 32 space}Total:    {paddedDuration}ms\")\n                    |> ignore\n\n                    sb.AppendLine(String.replicate 80 \"=\") |> ignore\n\n                // Write the timings.\n                if sb.Length > 0 then publishTimings sb\n        finally\n            stringBuilderPool.Return(sb)\n\n    let removeTiming correlationId =\n        let mutable x = null\n        timings.TryRemove(correlationId, &x) |> ignore\n"
  },
  {
    "path": "src/Grace.Actors/Types.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\nopen System.Runtime.CompilerServices\n\nmodule Types =\n\n    type TimingFlag =\n        | Initial\n        | BeforeRetrieveState\n        | AfterRetrieveState\n        | BeforeSaveState\n        | AfterSaveState\n        | BeforeStorageQuery\n        | AfterStorageQuery\n        | BeforeGettingCorrelationIdFromMemoryCache\n        | AfterGettingCorrelationIdFromMemoryCache\n        | BeforeSettingCorrelationIdInMemoryCache\n        | AfterSettingCorrelationIdInMemoryCache\n        | Final\n\n    type Timing =\n        {\n            Time: Instant\n            ActorStateName: string\n            Flag: TimingFlag\n        }\n\n        static member Create (flag: TimingFlag) actorStateName = { Time = getCurrentInstant (); ActorStateName = actorStateName; Flag = flag }\n"
  },
  {
    "path": "src/Grace.Actors/User.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen NodaTime\nopen System\n\nmodule User =\n\n    [<Serializable>]\n    type UserDto(userId, emailAddress, isPrivateDefault, isSuspended, isDeactivated, defaultTimeZone, updatedAt) =\n        new() = UserDto(Guid.NewGuid(), String.Empty, false, false, false, TimeZoneInfo.Utc.Id, getCurrentInstant ())\n        member val public UserId: Guid = userId with get, set\n        member val public EmailAddress: string = emailAddress with get, set\n        member val public IsPrivateDefault: bool = isPrivateDefault with get, set\n        member val public IsSuspended: bool = isSuspended with get, set\n        member val public IsDeactivated: bool = isDeactivated with get, set\n        member val public DefaultTimeZone: string = defaultTimeZone with get, set\n        member val public UpdatedAt: Instant = updatedAt with get, set\n\n\n    type User() =\n        let x = 0\n"
  },
  {
    "path": "src/Grace.Actors/ValidationResult.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Events\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule ValidationResult =\n    let internal hasDuplicateCorrelationId (events: seq<ValidationResultEvent>) (metadata: EventMetadata) =\n        events\n        |> Seq.exists (fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId)\n\n    type ValidationResultActor\n        (\n            [<PersistentState(StateName.ValidationResult, Constants.GraceActorStorage)>] state: IPersistentState<List<ValidationResultEvent>>\n        ) =\n        inherit Grain()\n\n        static let actorName = ActorName.ValidationResult\n        let log = loggerFactory.CreateLogger(\"ValidationResult.Actor\")\n\n        let mutable currentCommand = String.Empty\n        let mutable validationResult = ValidationResultDto.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            validationResult <-\n                state.State\n                |> Seq.fold (fun dto event -> ValidationResultDto.UpdateDto event dto) validationResult\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent(validationResultEvent: ValidationResultEvent) =\n            task {\n                let correlationId = validationResultEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(validationResultEvent)\n                    do! state.WriteStateAsync()\n\n                    validationResult <-\n                        validationResult\n                        |> ValidationResultDto.UpdateDto validationResultEvent\n\n                    let graceEvent = GraceEvent.ValidationResultEvent validationResultEvent\n                    do! publishGraceEvent graceEvent validationResultEvent.Metadata\n\n                    let graceReturnValue: GraceReturnValue<string> =\n                        (GraceReturnValue.Create \"Validation result command succeeded.\" correlationId)\n                            .enhance(nameof RepositoryId, validationResult.RepositoryId)\n                            .enhance (nameof ValidationResultId, validationResult.ValidationResultId)\n\n                    return Ok graceReturnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to apply event for ValidationResultId: {ValidationResultId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        validationResult.ValidationResultId\n                    )\n\n                    return\n                        Error(\n                            (GraceError.CreateWithException ex \"Failed while applying ValidationResult event.\" correlationId)\n                                .enhance(nameof RepositoryId, validationResult.RepositoryId)\n                                .enhance (nameof ValidationResultId, validationResult.ValidationResultId)\n                        )\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = validationResult.RepositoryId |> returnTask\n\n        interface IValidationResultActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                not\n                <| validationResult.ValidationResultId.Equals(ValidationResultId.Empty)\n                |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n\n                if validationResult.ValidationResultId = ValidationResultId.Empty then\n                    Option.None\n                else\n                    Some validationResult\n                |> returnTask\n\n            member this.GetEvents correlationId =\n                this.correlationId <- correlationId\n\n                state.State :> IReadOnlyList<ValidationResultEvent>\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (validationResultCommand: ValidationResultCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        if hasDuplicateCorrelationId state.State eventMetadata then\n                            return Error(GraceError.Create \"Duplicate correlation ID for ValidationResult command.\" eventMetadata.CorrelationId)\n                        else\n                            match validationResultCommand with\n                            | ValidationResultCommand.Record _ -> return Ok validationResultCommand\n                    }\n\n                let processCommand (validationResultCommand: ValidationResultCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        let eventType =\n                            match validationResultCommand with\n                            | ValidationResultCommand.Record validationResultDto -> ValidationResultEventType.Recorded validationResultDto\n\n                        let validationResultEvent: ValidationResultEvent = { Event = eventType; Metadata = eventMetadata }\n                        return! this.ApplyEvent validationResultEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n\n                    match! isValid command metadata with\n                    | Ok validCommand -> return! processCommand validCommand metadata\n                    | Error validationError -> return Error validationError\n                }\n"
  },
  {
    "path": "src/Grace.Actors/ValidationSet.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Events\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule ValidationSet =\n\n    type ValidationSetActor([<PersistentState(StateName.ValidationSet, Constants.GraceActorStorage)>] state: IPersistentState<List<ValidationSetEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.ValidationSet\n        let log = loggerFactory.CreateLogger(\"ValidationSet.Actor\")\n\n        let mutable currentCommand = String.Empty\n        let mutable validationSet = ValidationSetDto.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            validationSet <-\n                state.State\n                |> Seq.fold (fun dto event -> ValidationSetDto.UpdateDto event dto) validationSet\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent(validationSetEvent: ValidationSetEvent) =\n            task {\n                let correlationId = validationSetEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(validationSetEvent)\n                    do! state.WriteStateAsync()\n\n                    validationSet <-\n                        validationSet\n                        |> ValidationSetDto.UpdateDto validationSetEvent\n\n                    let graceEvent = GraceEvent.ValidationSetEvent validationSetEvent\n                    do! publishGraceEvent graceEvent validationSetEvent.Metadata\n\n                    let graceReturnValue: GraceReturnValue<string> =\n                        (GraceReturnValue.Create \"Validation set command succeeded.\" correlationId)\n                            .enhance(nameof RepositoryId, validationSet.RepositoryId)\n                            .enhance (nameof ValidationSetId, validationSet.ValidationSetId)\n\n                    return Ok graceReturnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to apply event for ValidationSetId: {ValidationSetId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        validationSet.ValidationSetId\n                    )\n\n                    return\n                        Error(\n                            (GraceError.CreateWithException ex \"Failed while applying ValidationSet event.\" correlationId)\n                                .enhance(nameof RepositoryId, validationSet.RepositoryId)\n                                .enhance (nameof ValidationSetId, validationSet.ValidationSetId)\n                        )\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = validationSet.RepositoryId |> returnTask\n\n        interface IValidationSetActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                not\n                <| validationSet.ValidationSetId.Equals(ValidationSetId.Empty)\n                |> returnTask\n\n            member this.IsDeleted correlationId =\n                this.correlationId <- correlationId\n                validationSet.DeletedAt.IsSome |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n\n                if validationSet.ValidationSetId = ValidationSetId.Empty then\n                    Option.None\n                else\n                    Some validationSet\n                |> returnTask\n\n            member this.GetEvents correlationId =\n                this.correlationId <- correlationId\n\n                state.State :> IReadOnlyList<ValidationSetEvent>\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (validationSetCommand: ValidationSetCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        if state.State.Exists(fun ev -> ev.Metadata.CorrelationId = eventMetadata.CorrelationId) then\n                            return Error(GraceError.Create \"Duplicate correlation ID for ValidationSet command.\" eventMetadata.CorrelationId)\n                        else\n                            match validationSetCommand with\n                            | ValidationSetCommand.Create _ when\n                                validationSet.ValidationSetId\n                                <> ValidationSetId.Empty\n                                ->\n                                return Error(GraceError.Create \"ValidationSet already exists.\" eventMetadata.CorrelationId)\n                            | _ -> return Ok validationSetCommand\n                    }\n\n                let processCommand (validationSetCommand: ValidationSetCommand) (eventMetadata: EventMetadata) =\n                    task {\n                        let eventType =\n                            match validationSetCommand with\n                            | ValidationSetCommand.Create validationSetDto -> ValidationSetEventType.Created validationSetDto\n                            | ValidationSetCommand.Update validationSetDto -> ValidationSetEventType.Updated validationSetDto\n                            | ValidationSetCommand.DeleteLogical (force, deleteReason) -> ValidationSetEventType.LogicalDeleted(force, deleteReason)\n\n                        let validationSetEvent: ValidationSetEvent = { Event = eventType; Metadata = eventMetadata }\n                        return! this.ApplyEvent validationSetEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n\n                    match! isValid command metadata with\n                    | Ok validCommand -> return! processCommand validCommand metadata\n                    | Error validationError -> return Error validationError\n                }\n"
  },
  {
    "path": "src/Grace.Actors/WorkItem.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Events\nopen Grace.Types.Types\nopen Grace.Types.WorkItem\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule WorkItem =\n\n    let internal hasDuplicateCorrelationId (events: seq<WorkItemEvent>) (metadata: EventMetadata) =\n        events\n        |> Seq.exists (fun ev -> ev.Metadata.CorrelationId = metadata.CorrelationId)\n\n\n    type WorkItemActor([<PersistentState(StateName.WorkItem, Constants.GraceActorStorage)>] state: IPersistentState<List<WorkItemEvent>>) =\n        inherit Grain()\n\n        static let actorName = ActorName.WorkItem\n\n        let log = loggerFactory.CreateLogger(\"WorkItem.Actor\")\n\n        let mutable currentCommand = String.Empty\n\n        let mutable workItemDto = WorkItemDto.Default\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            workItemDto <-\n                state.State\n                |> Seq.fold (fun dto ev -> WorkItemDto.UpdateDto ev dto) workItemDto\n\n            Task.CompletedTask\n\n        member private this.ApplyEvent(workItemEvent: WorkItemEvent) =\n            task {\n                let correlationId = workItemEvent.Metadata.CorrelationId\n\n                try\n                    state.State.Add(workItemEvent)\n                    do! state.WriteStateAsync()\n\n                    workItemDto <- workItemDto |> WorkItemDto.UpdateDto workItemEvent\n\n                    let graceEvent = GraceEvent.WorkItemEvent workItemEvent\n                    do! publishGraceEvent graceEvent workItemEvent.Metadata\n\n                    let returnValue =\n                        (GraceReturnValue.Create \"Work item command succeeded.\" correlationId)\n                            .enhance(nameof RepositoryId, workItemDto.RepositoryId)\n                            .enhance(nameof WorkItemId, workItemDto.WorkItemId)\n                            .enhance (nameof WorkItemEventType, getDiscriminatedUnionFullName workItemEvent.Event)\n\n                    return Ok returnValue\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Failed to apply event {eventType} for work item {workItemId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        getDiscriminatedUnionCaseName workItemEvent.Event,\n                        workItemDto.WorkItemId\n                    )\n\n                    let graceError =\n                        (GraceError.CreateWithException ex (WorkItemError.getErrorMessage WorkItemError.FailedWhileApplyingEvent) correlationId)\n                            .enhance (nameof WorkItemId, workItemDto.WorkItemId)\n\n                    return Error graceError\n            }\n\n        interface IHasRepositoryId with\n            member this.GetRepositoryId correlationId = workItemDto.RepositoryId |> returnTask\n\n        interface IWorkItemActor with\n            member this.Exists correlationId =\n                this.correlationId <- correlationId\n\n                not\n                <| workItemDto.WorkItemId.Equals(WorkItemDto.Default.WorkItemId)\n                |> returnTask\n\n            member this.Get correlationId =\n                this.correlationId <- correlationId\n                workItemDto |> returnTask\n\n            member this.GetEvents correlationId =\n                this.correlationId <- correlationId\n\n                state.State :> IReadOnlyList<WorkItemEvent>\n                |> returnTask\n\n            member this.Handle command metadata =\n                let isValid (command: WorkItemCommand) (metadata: EventMetadata) =\n                    task {\n                        if hasDuplicateCorrelationId state.State metadata then\n                            return Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.DuplicateCorrelationId) metadata.CorrelationId)\n                        else\n                            match command with\n                            | Create _ ->\n                                if workItemDto.WorkItemId <> WorkItemId.Empty then\n                                    return Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.WorkItemAlreadyExists) metadata.CorrelationId)\n                                else\n                                    return Ok command\n                            | _ ->\n                                if workItemDto.WorkItemId = WorkItemId.Empty then\n                                    return Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.WorkItemDoesNotExist) metadata.CorrelationId)\n                                else\n                                    return Ok command\n                    }\n\n                let processCommand (command: WorkItemCommand) (metadata: EventMetadata) =\n                    task {\n                        let! workItemEventType =\n                            task {\n                                match command with\n                                | Create (workItemId, workItemNumber, ownerId, organizationId, repositoryId, title, description) ->\n                                    return Created(workItemId, workItemNumber, ownerId, organizationId, repositoryId, title, description)\n                                | SetTitle title -> return TitleSet title\n                                | SetDescription description -> return DescriptionSet description\n                                | SetStatus status -> return StatusSet status\n                                | AddParticipant userId -> return ParticipantAdded userId\n                                | RemoveParticipant userId -> return ParticipantRemoved userId\n                                | AddTag tag -> return TagAdded tag\n                                | RemoveTag tag -> return TagRemoved tag\n                                | SetConstraints constraints -> return ConstraintsSet constraints\n                                | SetNotes notes -> return NotesSet notes\n                                | SetArchitecturalNotes notes -> return ArchitecturalNotesSet notes\n                                | SetMigrationNotes notes -> return MigrationNotesSet notes\n                                | AddExternalRef reference -> return ExternalRefAdded reference\n                                | RemoveExternalRef reference -> return ExternalRefRemoved reference\n                                | LinkBranch branchId -> return BranchLinked branchId\n                                | UnlinkBranch branchId -> return BranchUnlinked branchId\n                                | LinkReference referenceId -> return ReferenceLinked referenceId\n                                | UnlinkReference referenceId -> return ReferenceUnlinked referenceId\n                                | LinkArtifact artifactId -> return ArtifactLinked artifactId\n                                | UnlinkArtifact artifactId -> return ArtifactUnlinked artifactId\n                                | LinkPromotionSet promotionSetId -> return PromotionSetLinked promotionSetId\n                                | UnlinkPromotionSet promotionSetId -> return PromotionSetUnlinked promotionSetId\n                                | LinkReviewNotes reviewNotesId -> return ReviewNotesLinked reviewNotesId\n                                | UnlinkReviewNotes reviewNotesId -> return ReviewNotesUnlinked reviewNotesId\n                                | LinkReviewCheckpoint reviewCheckpointId -> return ReviewCheckpointLinked reviewCheckpointId\n                                | UnlinkReviewCheckpoint reviewCheckpointId -> return ReviewCheckpointUnlinked reviewCheckpointId\n                                | LinkValidationResult validationResultId -> return ValidationResultLinked validationResultId\n                                | UnlinkValidationResult validationResultId -> return ValidationResultUnlinked validationResultId\n                            }\n\n                        let workItemEvent = { Event = workItemEventType; Metadata = metadata }\n                        return! this.ApplyEvent workItemEvent\n                    }\n\n                task {\n                    currentCommand <- getDiscriminatedUnionCaseName command\n                    this.correlationId <- metadata.CorrelationId\n                    RequestContext.Set(Constants.CurrentCommandProperty, getDiscriminatedUnionCaseName command)\n\n                    match! isValid command metadata with\n                    | Ok command -> return! processCommand command metadata\n                    | Error error -> return Error error\n                }\n"
  },
  {
    "path": "src/Grace.Actors/WorkItemNumber.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule WorkItemNumber =\n\n    type WorkItemNumberActor() =\n        inherit Grain()\n\n        static let actorName = ActorName.WorkItemNumber\n\n        let log = loggerFactory.CreateLogger(\"WorkItemNumber.Actor\")\n\n        let cachedWorkItemIds = Dictionary<WorkItemNumber, WorkItemId>()\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n            logActorActivation log this.IdentityString activateStartTime \"In-memory only\"\n            Task.CompletedTask\n\n        interface IWorkItemNumberActor with\n            member this.GetWorkItemId(workItemNumber: WorkItemNumber) correlationId =\n                this.correlationId <- correlationId\n\n                match cachedWorkItemIds.TryGetValue workItemNumber with\n                | true, workItemId -> Some workItemId |> returnTask\n                | false, _ -> None |> returnTask\n\n            member this.SetWorkItemId(workItemNumber: WorkItemNumber) (workItemId: WorkItemId) correlationId =\n                this.correlationId <- correlationId\n\n                if workItemNumber > 0L && workItemId <> Guid.Empty then\n                    cachedWorkItemIds[workItemNumber] <- workItemId\n\n                Task.CompletedTask\n"
  },
  {
    "path": "src/Grace.Actors/WorkItemNumberCounter.Actor.fs",
    "content": "namespace Grace.Actors\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Shared.Constants\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Threading.Tasks\n\nmodule WorkItemNumberCounter =\n\n    [<CLIMutable>]\n    type WorkItemNumberCounterState = { NextWorkItemNumber: WorkItemNumber }\n\n    type WorkItemNumberCounterActor(\n        [<PersistentState(StateName.WorkItemNumberCounter, Grace.Shared.Constants.GraceActorStorage)>]\n        state: IPersistentState<WorkItemNumberCounterState>\n    ) =\n        inherit Grain()\n\n        static let actorName = ActorName.WorkItemNumberCounter\n\n        let log = loggerFactory.CreateLogger(\"WorkItemNumberCounter.Actor\")\n\n        member val private correlationId: CorrelationId = String.Empty with get, set\n\n        override this.OnActivateAsync(ct) =\n            let activateStartTime = getCurrentInstant ()\n            logActorActivation log this.IdentityString activateStartTime (getActorActivationMessage state.RecordExists)\n\n            if not state.RecordExists then\n                state.State <- { NextWorkItemNumber = 1L }\n\n            Task.CompletedTask\n\n        interface IWorkItemNumberCounterActor with\n            member this.AllocateNext(correlationId: CorrelationId) =\n                task {\n                    this.correlationId <- correlationId\n\n                    if not state.RecordExists && state.State.NextWorkItemNumber <= 0L then\n                        state.State <- { NextWorkItemNumber = 1L }\n\n                    let nextWorkItemNumber =\n                        if state.State.NextWorkItemNumber > 0L then\n                            state.State.NextWorkItemNumber\n                        else\n                            1L\n\n                    state.State <- { NextWorkItemNumber = nextWorkItemNumber + 1L }\n                    do! state.WriteStateAsync()\n\n                    return nextWorkItemNumber\n                }\n"
  },
  {
    "path": "src/Grace.Aspire.AppHost/AGENTS.md",
    "content": "# Grace.Aspire.AppHost Agent Notes\n\n- AppHost reads the Grace.Server user-secrets ID and forwards selected auth\n  settings to the `grace-server` project.\n- Auth0/OIDC settings are expected under the raw keys (with `__`) in user\n  secrets or env vars: `grace__auth__oidc__authority`,\n  `grace__auth__oidc__audience`.\n- When troubleshooting missing auth providers in Aspire, check the\n  `grace-server` environment list in the dashboard and the AppHost startup log\n  line that summarizes forwarded auth keys.\n"
  },
  {
    "path": "src/Grace.Aspire.AppHost/Grace.Aspire.AppHost.csproj",
    "content": "<Project Sdk=\"Aspire.AppHost.Sdk/13.1.0\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <UserSecretsId>f1167a88-7f15-49c3-8ea1-30c2608081c9</UserSecretsId>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.Hosting\" Version=\"13.1.0\" />\n    <PackageReference Include=\"Aspire.Hosting.Azure.CosmosDB\" Version=\"13.1.0\" />\n    <PackageReference Include=\"Aspire.Hosting.Azure.ServiceBus\" Version=\"13.1.0\" />\n    <PackageReference Include=\"Aspire.Hosting.Azure.Storage\" Version=\"13.1.0\" />\n    <PackageReference Include=\"Aspire.Hosting.Redis\" Version=\"13.1.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Grace.Server\\Grace.Server.fsproj\" />\n    <ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" IsAspireProjectResource=\"false\">\n        <Aliases>Shared</Aliases>\n    </ProjectReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Update=\"appsettings.Development.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Aspire.AppHost/Program.Aspire.AppHost.cs",
    "content": "extern alias Shared;\n\nusing Aspire.Hosting;\nusing Aspire.Hosting.ApplicationModel;\nusing Aspire.Hosting.Azure;\nusing Aspire.Hosting.Redis;\nusing Grace.Shared;\nusing static Grace.Shared.Utilities;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text.Json;\nusing static Grace.Types.Types;\nusing static Shared::Grace.Shared.Constants;\n\npublic partial class Program\n{\n    private const string AspireResourceModeEnvVar = \"ASPIRE_RESOURCE_MODE\";\n    private const string AspireResourceModeLocal = \"Local\";\n    private const string AspireResourceModeAzure = \"Azure\";\n\n    private static void Main(string[] args)\n    {\n        try\n        {\n            var environmentName = Environment.GetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\") ?? \"Development\";\n\n            var configuration = new ConfigurationBuilder()\n                .AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true)\n                .AddJsonFile($\"appsettings.{environmentName}.json\", optional: true, reloadOnChange: true)\n                .AddUserSecrets(typeof(Program).Assembly, optional: true)\n                .AddEnvironmentVariables()\n                .Build();\n\n            IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(args);\n\n            // Run-mode switch:\n            //   - Local (default): containers/emulators for Azurite, Cosmos emulator, ServiceBus emulator\n            //   - Azure: debug locally, but use real Azure resources (connection strings from config/env/user-secrets)\n            var resourceMode =\n                Environment.GetEnvironmentVariable(AspireResourceModeEnvVar)\n                ?? configuration[$\"grace:{AspireResourceModeEnvVar}\"]\n                ?? AspireResourceModeLocal;\n\n            var isRunMode = builder.ExecutionContext.IsRunMode;\n            var isPublishMode = builder.ExecutionContext.IsPublishMode;\n            Console.WriteLine($\"Aspire execution context: Run={isRunMode}; Publish={isPublishMode}.\");\n            var isAzureDebugRun =\n                isRunMode &&\n                resourceMode.Equals(AspireResourceModeAzure, StringComparison.OrdinalIgnoreCase);\n            var isTestRun =\n                Environment.GetEnvironmentVariable(\"GRACE_TESTING\") is string testValue\n                && (testValue.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                    || testValue.Equals(\"true\", StringComparison.OrdinalIgnoreCase));\n            var runSuffix = isTestRun\n                ? (Environment.GetEnvironmentVariable(\"GRACE_TEST_RUN_ID\") ?? Guid.NewGuid().ToString(\"N\"))\n                : null;\n            static bool IsTruthy(string? value) =>\n                !string.IsNullOrWhiteSpace(value)\n                && (value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                    || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n                    || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase));\n            var skipServiceBus = isTestRun && IsTruthy(Environment.GetEnvironmentVariable(\"GRACE_TEST_SKIP_SERVICEBUS\"));\n            var useFixedTestPorts = isTestRun && IsTruthy(Environment.GetEnvironmentVariable(\"GRACE_TEST_FIXED_PORTS\"));\n            var pubSubSystem = skipServiceBus ? \"UnknownPubSubProvider\" : \"AzureServiceBus\";\n            if (isTestRun)\n            {\n                CleanupDockerContainers(new[] { \"servicebus-sql\", \"servicebus-emulator\" });\n            }\n\n            // Redis: keep local container for both run modes (Local + Azure debug), and even in publish mode if you like.\n            var redisContainerName = runSuffix is null ? \"redis\" : $\"redis-{runSuffix}\";\n            var redis = builder.AddContainer(\"redis\", \"redis\", \"latest\")\n                .WithContainerName(redisContainerName)\n                //.WithLifetime(ContainerLifetime.Session)\n                .WithEnvironment(\"ACCEPT_EULA\", \"Y\")\n                .WithEndpoint(targetPort: 6379, port: 6379);\n            if (isTestRun)\n            {\n                redis.WithLifetime(ContainerLifetime.Session);\n            }\n\n            if (!isPublishMode)\n            {\n                // =========================\n                // RUN MODE (debug / local)\n                // =========================\n\n                // Common settings for local debugging\n                var otlpEndpoint = configuration[\"grace:otlp_endpoint\"] ?? \"http://localhost:18889\";\n                var stateRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), \".grace\", \"aspire\");\n                var logDirectory = Path.Combine(stateRoot, \"logs\");\n\n                Directory.CreateDirectory(stateRoot);\n                Directory.CreateDirectory(logDirectory);\n\n                // These get set in both Local and Azure-debug runs.\n                var orleansClusterId = configuration[getConfigKey(EnvironmentVariables.OrleansClusterId)] ?? \"local\";\n                var orleansServiceId = configuration[getConfigKey(EnvironmentVariables.OrleansServiceId)] ?? \"grace-dev\";\n\n                if (isTestRun && runSuffix is not null)\n                {\n                    orleansClusterId = $\"{orleansClusterId}-test-{runSuffix}\";\n                    orleansServiceId = $\"{orleansServiceId}-test-{runSuffix}\";\n                }\n\n                Console.WriteLine($\"Using Orleans ClusterId='{orleansClusterId}' and ServiceId='{orleansServiceId}'.\");\n\n                var graceServer = builder.AddProject(\"grace-server\", \"..\\\\Grace.Server\\\\Grace.Server.fsproj\")\n                    .WithParentRelationship(redis)\n                    .WithEnvironment(\"ASPNETCORE_ENVIRONMENT\", \"Development\")\n                    .WithEnvironment(\"DOTNET_ENVIRONMENT\", \"Development\")\n                    .WithEnvironment(\"OTLP_ENDPOINT_URL\", otlpEndpoint)\n                    .WithEnvironment(EnvironmentVariables.ApplicationInsightsConnectionString, configuration[getConfigKey(EnvironmentVariables.ApplicationInsightsConnectionString)] ?? string.Empty)\n                    .WithEnvironment(EnvironmentVariables.DirectoryVersionContainerName, \"directoryversions\")\n                    .WithEnvironment(EnvironmentVariables.DiffContainerName, \"diffs\")\n                    .WithEnvironment(EnvironmentVariables.ZipFileContainerName, \"zipfiles\")\n                    .WithEnvironment(EnvironmentVariables.RedisHost, \"127.0.0.1\")\n                    .WithEnvironment(EnvironmentVariables.RedisPort, \"6379\")\n                    .WithEnvironment(EnvironmentVariables.OrleansClusterId, orleansClusterId)\n                    .WithEnvironment(EnvironmentVariables.OrleansServiceId, orleansServiceId)\n                    .WithEnvironment(EnvironmentVariables.GracePubSubSystem, pubSubSystem)\n                    .AsHttp2Service()\n                    .WithOtlpExporter();\n\n                var forwardedAuthKeys = new List<string>();\n                AddOptionalEnvironment(graceServer, configuration, EnvironmentVariables.GraceAuthOidcAuthority, forwardedAuthKeys);\n                AddOptionalEnvironment(graceServer, configuration, EnvironmentVariables.GraceAuthOidcAudience, forwardedAuthKeys);\n                LogForwardedSettings(\"Grace.Server auth settings\", forwardedAuthKeys);\n\n                if (isTestRun && !useFixedTestPorts)\n                {\n                    var graceTargetPort = GetAvailableTcpPort();\n\n                    graceServer\n                        .WithHttpEndpoint(targetPort: graceTargetPort, name: \"http\")\n                        .WithEnvironment(\"ASPNETCORE_URLS\", \"http://127.0.0.1:\" + graceTargetPort);\n                }\n                else\n                {\n                    graceServer\n                        .WithEnvironment(\"ASPNETCORE_URLS\", \"https://+:5001;http://+:5000\")\n                        .WithEnvironment(EnvironmentVariables.GraceServerUri, \"http://localhost:5000\")\n                        .WithHttpEndpoint(targetPort: 5000, name: \"http\")\n                        .WithHttpsEndpoint(targetPort: 5001, name: \"https\");\n                }\n\n                if (!isAzureDebugRun)\n                {\n                    // -------------------------\n                    // DebugLocal (default): containers/emulators\n                    // -------------------------\n                    Console.WriteLine(\"Configuring Grace.Server for DebugLocal with local emulators.\");\n                    var azuriteDataPath = Path.Combine(stateRoot, \"azurite\");\n                    var cosmosCertPath = Path.Combine(stateRoot, \"cosmos-cert\");\n                    var serviceBusConfigPath = Path.Combine(stateRoot, \"servicebus\");\n\n                Directory.CreateDirectory(azuriteDataPath);\n                Directory.CreateDirectory(cosmosCertPath);\n                Directory.CreateDirectory(serviceBusConfigPath);\n\n                // Create Service Bus emulator config (when enabled for tests)\n                string? serviceBusConfigFile = null;\n                if (!skipServiceBus)\n                {\n                    serviceBusConfigFile = Path.Combine(\n                        serviceBusConfigPath,\n                        $\"config_{Process.GetCurrentProcess().Id}_{Guid.NewGuid():N}.json\");\n                    CreateServiceBusConfiguration(serviceBusConfigFile, configuration);\n                }\n\n                    var azuriteContainerName = runSuffix is null ? \"azurite\" : $\"azurite-{runSuffix}\";\n                    var azurite = builder.AddContainer(\"azurite\", \"mcr.microsoft.com/azure-storage/azurite\", \"latest\")\n                        .WithContainerName(azuriteContainerName)\n                        .WithBindMount(azuriteDataPath, \"/data\")\n                        //.WithLifetime(ContainerLifetime.Session)\n                        .WithEnvironment(\"AZURITE_ACCOUNTS\", \"gracevcsdevelopment:Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==\")\n                        .WithEndpoint(targetPort: 10000, port: 10000, name: \"blob\", scheme: \"http\")\n                        .WithEndpoint(targetPort: 10001, port: 10001, name: \"queue\", scheme: \"http\")\n                        .WithEndpoint(targetPort: 10002, port: 10002, name: \"table\", scheme: \"http\");\n                    if (isTestRun)\n                    {\n                        azurite.WithLifetime(ContainerLifetime.Session);\n                    }\n                    var azuriteBlobEndpoint = azurite.GetEndpoint(\"blob\");\n                    var azuriteQueueEndpoint = azurite.GetEndpoint(\"queue\");\n                    var azuriteTableEndpoint = azurite.GetEndpoint(\"table\");\n                    var azuriteBlobHostAndPort = azuriteBlobEndpoint.Property(EndpointProperty.HostAndPort);\n                    var azuriteQueueHostAndPort = azuriteQueueEndpoint.Property(EndpointProperty.HostAndPort);\n                    var azuriteTableHostAndPort = azuriteTableEndpoint.Property(EndpointProperty.HostAndPort);\n\n                    // Cosmos emulator (your existing approach)\n                    const string cosmosKey = \"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==\";\n                    var cosmosDatabaseName = configuration[getConfigKey(EnvironmentVariables.AzureCosmosDBDatabaseName)] ?? \"grace-dev\";\n                    var cosmosDbContainerName = configuration[getConfigKey(EnvironmentVariables.AzureCosmosDBContainerName)] ?? \"grace-events\";\n                    const int cosmosGatewayHostPort = 8081;\n\n#pragma warning disable ASPIRECOSMOSDB001\n                    var cosmosEmulatorContainerName = runSuffix is null ? \"cosmosdb-emulator\" : $\"cosmosdb-emulator-{runSuffix}\";\n                    var cosmos = builder.AddAzureCosmosDB(\"cosmos\")\n                        .RunAsPreviewEmulator(emulator =>\n                        {\n                            emulator\n                                .WithContainerName(cosmosEmulatorContainerName)\n                                //.WithLifetime(ContainerLifetime.Session)\n                                .WithEnvironment(\"ACCEPT_EULA\", \"Y\")\n                                .WithEnvironment(\"AZURE_COSMOS_EMULATOR_PARTITION_COUNT\", \"10\")\n                                .WithEnvironment(\"AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE\", \"false\")\n                                .WithEnvironment(\"AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE\", \"127.0.0.1\")\n                                .WithEnvironment(\"ENABLE_OTLP_EXPORTER\", \"true\")\n                                .WithEnvironment(\"LOG_LEVEL\", \"info\")\n                                .WithDataExplorer(1234)\n                                .WithGatewayPort(cosmosGatewayHostPort);\n\n                            if (isTestRun)\n                            {\n                                emulator.WithLifetime(ContainerLifetime.Session);\n                            }\n                        });\n#pragma warning restore ASPIRECOSMOSDB001\n\n                    _ = cosmos.AddCosmosDatabase(cosmosDatabaseName)\n                        .AddContainer(cosmosDbContainerName, \"/PartitionKey\");\n                    var cosmosConnStr = BuildCosmosEmulatorConnectionString(cosmos, cosmosKey);\n\n                    graceServer\n                        .WithParentRelationship(azurite)\n                        .WithParentRelationship(cosmos)\n                        .WithEnvironment(\n                            EnvironmentVariables.AzureStorageConnectionString,\n                            ReferenceExpression.Create(\n                                $\"DefaultEndpointsProtocol=http;AccountName=gracevcsdevelopment;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://{azuriteBlobHostAndPort}/gracevcsdevelopment;QueueEndpoint=http://{azuriteQueueHostAndPort}/gracevcsdevelopment;TableEndpoint=http://{azuriteTableHostAndPort}/gracevcsdevelopment;\")\n                        )\n                        .WithEnvironment(EnvironmentVariables.AzureStorageKey,\n                            \"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==\")\n                        .WithEnvironment(EnvironmentVariables.AzureCosmosDBConnectionString, cosmosConnStr)\n                        .WithEnvironment(EnvironmentVariables.AzureCosmosDBDatabaseName, cosmosDatabaseName)\n                        .WithEnvironment(EnvironmentVariables.AzureCosmosDBContainerName, cosmosDbContainerName)\n                        .WithEnvironment(EnvironmentVariables.GraceLogDirectory, logDirectory)\n                        .WithEnvironment(EnvironmentVariables.DebugEnvironment, \"Local\");\n\n                    if (!skipServiceBus)\n                    {\n                        // Service Bus emulator\n                        var serviceBusSqlPassword = configuration[\"grace:azure_service_bus:sqlpassword\"] ?? \"SqlIsAwesome1!\";\n                        var serviceBusConfigFilePath =\n                            serviceBusConfigFile ?? throw new InvalidOperationException(\"Service Bus config file was not created.\");\n\n                        var serviceBusSqlResourceName = runSuffix is null ? \"servicebus-sql\" : $\"servicebus-sql-{runSuffix}\";\n                        var serviceBusSql = builder.AddContainer(serviceBusSqlResourceName, \"mcr.microsoft.com/mssql/server\", \"2022-latest\")\n                            .WithContainerName(serviceBusSqlResourceName)\n                            .WithEnvironment(\"ACCEPT_EULA\", \"Y\")\n                            .WithEnvironment(\"MSSQL_SA_PASSWORD\", serviceBusSqlPassword)\n                            //.WithLifetime(ContainerLifetime.Session)\n                            .WithEndpoint(targetPort: 1433, port: 21433, name: \"sql\", scheme: \"tcp\");\n                        if (isTestRun)\n                        {\n                            serviceBusSql.WithEnvironment(\"MSSQL_MEMORY_LIMIT_MB\", \"1024\");\n                        }\n                        else\n                        {\n                            var memoryLimit = configuration[\"grace:azure_service_bus:sqlmemory\"];\n                            if (!string.IsNullOrWhiteSpace(memoryLimit))\n                            {\n                                serviceBusSql.WithEnvironment(\"MSSQL_MEMORY_LIMIT_MB\", memoryLimit);\n                            }\n                        }\n                        if (isTestRun)\n                        {\n                            serviceBusSql.WithLifetime(ContainerLifetime.Session);\n                        }\n\n                        var serviceBusEmulatorResourceName = runSuffix is null ? \"servicebus-emulator\" : $\"servicebus-emulator-{runSuffix}\";\n                        var serviceBusEmulator = builder.AddContainer(serviceBusEmulatorResourceName, \"mcr.microsoft.com/azure-messaging/servicebus-emulator\", \"latest\")\n                            .WithContainerName(serviceBusEmulatorResourceName)\n                            .WithParentRelationship(serviceBusSql)\n                            .WithEnvironment(\"ACCEPT_EULA\", \"Y\")\n                            .WithEnvironment(\"MSSQL_SA_PASSWORD\", serviceBusSqlPassword)\n                            .WithEnvironment(\"SQL_SERVER\", serviceBusSqlResourceName)\n                            .WithEnvironment(\"SQL_WAIT_INTERVAL\", \"10\")\n                            //.WithLifetime(ContainerLifetime.Session)\n                            .WithBindMount(serviceBusConfigFilePath, \"/ServiceBus_Emulator/ConfigFiles/Config.json\")\n                            .WithEndpoint(targetPort: 5672, port: 5672, name: \"amqp\", scheme: \"amqp\")\n                            .WithEndpoint(targetPort: 5300, port: 5300, name: \"management\", scheme: \"http\");\n                        if (isTestRun)\n                        {\n                            serviceBusEmulator.WithLifetime(ContainerLifetime.Session);\n                        }\n\n                        var serviceBusAmqpEndpoint = serviceBusEmulator.GetEndpoint(\"amqp\");\n                        var serviceBusHostAndPort = serviceBusAmqpEndpoint.Property(EndpointProperty.HostAndPort);\n\n                        graceServer\n                            .WithParentRelationship(serviceBusEmulator)\n                            .WithEnvironment(\n                                EnvironmentVariables.AzureServiceBusConnectionString,\n                                ReferenceExpression.Create(\n                                    $\"Endpoint=sb://{serviceBusHostAndPort};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;\"\n                                )\n                            )\n                            .WithEnvironment(EnvironmentVariables.AzureServiceBusNamespace, \"sbemulatorns\")\n                            .WithEnvironment(EnvironmentVariables.AzureServiceBusTopic, \"graceeventstream\")\n                            .WithEnvironment(EnvironmentVariables.AzureServiceBusSubscription, \"grace-server\");\n                    }\n                    else\n                    {\n                        Console.WriteLine(\"Skipping Service Bus emulator for this test run (GRACE_TEST_SKIP_SERVICEBUS=1).\");\n                    }\n\n                    Console.WriteLine(\"Grace.Server DebugLocal environment configured:\");\n                    Console.WriteLine(\"  - Azurite at http://localhost:10000-10002\");\n                    Console.WriteLine($\"  - Azurite data at {azuriteDataPath}\");\n                    Console.WriteLine(\"  - Cosmos emulator at http://localhost:8081\");\n                    if (!skipServiceBus)\n                    {\n                        Console.WriteLine(\"  - Service Bus emulator at amqp://localhost:5672\");\n                        Console.WriteLine($\"  - Service Bus config at {serviceBusConfigFile}\");\n                    }\n                    Console.WriteLine(\"  - Aspire dashboard at http://localhost:18888\");\n                    Console.WriteLine($\"  - OTLP endpoint {otlpEndpoint}\");\n                }\n                else\n                {\n                    // -------------------------\n                    // DebugAzure: still run locally under debugger, but use REAL Azure resources\n                    // -------------------------\n\n                    Console.WriteLine(\"Configuring Grace.Server for DebugAzure with real Azure resources.\");\n                    var azureStorageConnectionString = ResolveSetting(configuration, EnvironmentVariables.AzureStorageConnectionString);\n                    var azureStorageAccountName = ResolveSetting(configuration, EnvironmentVariables.AzureStorageAccountName);\n\n                    if (string.IsNullOrWhiteSpace(azureStorageConnectionString))\n                    {\n                        azureStorageAccountName = GetRequiredSetting(configuration, EnvironmentVariables.AzureStorageAccountName);\n                        Console.WriteLine($\"Using Azure Storage account: {azureStorageAccountName}.\");\n                    }\n                    else\n                    {\n                        Console.WriteLine(\"Using Azure Storage connection string from configuration.\");\n                    }\n\n                    var cosmosdbEndpoint = GetRequiredSetting(configuration, EnvironmentVariables.AzureCosmosDBEndpoint);\n                    Console.WriteLine($\"Using Cosmos DB endpoint: {cosmosdbEndpoint}.\");\n\n                    var cosmosDatabaseName = GetRequiredSetting(configuration, EnvironmentVariables.AzureCosmosDBDatabaseName);\n                    var cosmosContainerName = GetRequiredSetting(configuration, EnvironmentVariables.AzureCosmosDBContainerName);\n                    var serviceBusNamespace = ResolveSetting(configuration, EnvironmentVariables.AzureServiceBusNamespace);\n                    var serviceBusTopic = ResolveSetting(configuration, EnvironmentVariables.AzureServiceBusTopic);\n                    var serviceBusSubscription = ResolveSetting(configuration, EnvironmentVariables.AzureServiceBusSubscription);\n\n                    graceServer\n                        .WithEnvironment(EnvironmentVariables.AzureStorageAccountName, azureStorageAccountName)\n                        .WithEnvironment(EnvironmentVariables.AzureStorageConnectionString, azureStorageConnectionString)\n                        .WithEnvironment(EnvironmentVariables.AzureCosmosDBEndpoint, cosmosdbEndpoint)\n                        .WithEnvironment(EnvironmentVariables.AzureCosmosDBDatabaseName, cosmosDatabaseName)\n                        .WithEnvironment(EnvironmentVariables.AzureCosmosDBContainerName, cosmosContainerName)\n                        .WithEnvironment(EnvironmentVariables.AzureServiceBusNamespace, serviceBusNamespace)\n                        .WithEnvironment(EnvironmentVariables.AzureServiceBusTopic, serviceBusTopic)\n                        .WithEnvironment(EnvironmentVariables.AzureServiceBusSubscription, serviceBusSubscription)\n                        .WithEnvironment(EnvironmentVariables.GraceLogDirectory, logDirectory)\n                        .WithEnvironment(EnvironmentVariables.DebugEnvironment, \"Azure\");\n\n                    Console.WriteLine(\"Grace.Server DebugAzure environment configured (no emulators started):\");\n                    Console.WriteLine(\"  - Azure Storage: using DefaultAzureCredential.\");\n                    Console.WriteLine(\"  - Azure Cosmos: using DefaultAzureCredential.\");\n                    Console.WriteLine(\"  - Azure Service Bus: using DefaultAzureCredential.\");\n                    Console.WriteLine(\"  - Aspire dashboard at http://localhost:18888\");\n                    Console.WriteLine($\"  - OTLP endpoint {otlpEndpoint}\");\n                }\n            }\n            else\n            {\n                // =========================\n                // PUBLISH MODE (deployment model)\n                // =========================\n\n                var cosmos = builder.AddAzureCosmosDB(\"cosmos\");\n                var cosmosDatabase = cosmos.AddCosmosDatabase(configuration[getConfigKey(EnvironmentVariables.AzureCosmosDBDatabaseName)] ?? \"grace-dev\");\n                _ = cosmosDatabase.AddContainer(configuration[getConfigKey(EnvironmentVariables.AzureCosmosDBContainerName)] ?? \"grace-events\", \"/PartitionKey\");\n\n                var storage = builder.AddAzureStorage(\"storage\");\n                var blobStorage = storage.AddBlobContainer(\"directoryversions\");\n                var diffStorage = storage.AddBlobContainer(\"diffs\");\n                var zipStorage = storage.AddBlobContainer(\"zipfiles\");\n\n                var serviceBus = builder.AddAzureServiceBus(\"servicebus\");\n                _ = serviceBus.AddServiceBusTopic(configuration[getConfigKey(EnvironmentVariables.AzureServiceBusTopic)] ?? \"graceeventstream\")\n                    .AddServiceBusSubscription(configuration[getConfigKey(EnvironmentVariables.AzureServiceBusSubscription)] ?? \"grace-server\");\n\n                var otlpEndpoint = configuration[\"grace:otlp_endpoint\"] ?? \"http://localhost:18889\";\n                var publishLogDirectory = configuration[\"grace:log_directory\"] ?? \"/tmp/grace-logs\";\n\n                var graceServer = builder.AddProject(\"grace-server\", \"..\\\\Grace.Server\\\\Grace.Server.fsproj\")\n                    .WithReference(cosmosDatabase)\n                    .WithReference(blobStorage)\n                    .WithReference(diffStorage)\n                    .WithReference(zipStorage)\n                    .WithReference(serviceBus)\n                    .WithParentRelationship(redis)\n                    .WithEnvironment(\"ASPNETCORE_ENVIRONMENT\", \"Production\")\n                    .WithEnvironment(\"DOTNET_ENVIRONMENT\", \"Production\")\n                    .WithEnvironment(\"OTLP_ENDPOINT_URL\", otlpEndpoint)\n                    .WithEnvironment(EnvironmentVariables.ApplicationInsightsConnectionString, configuration[getConfigKey(EnvironmentVariables.ApplicationInsightsConnectionString)] ?? string.Empty)\n                    .WithEnvironment(EnvironmentVariables.GraceServerUri, configuration[getConfigKey(EnvironmentVariables.GraceServerUri)] ?? \"https://localhost:5001\")\n                    .WithEnvironment(EnvironmentVariables.AzureCosmosDBDatabaseName, configuration[getConfigKey(EnvironmentVariables.AzureCosmosDBDatabaseName)] ?? \"grace-dev\")\n                    .WithEnvironment(EnvironmentVariables.AzureCosmosDBContainerName, configuration[getConfigKey(EnvironmentVariables.AzureCosmosDBContainerName)] ?? \"grace-events\")\n                    .WithEnvironment(EnvironmentVariables.DirectoryVersionContainerName, \"directoryversions\")\n                    .WithEnvironment(EnvironmentVariables.DiffContainerName, \"diffs\")\n                    .WithEnvironment(EnvironmentVariables.ZipFileContainerName, \"zipfiles\")\n                    .WithEnvironment(EnvironmentVariables.RedisHost, \"localhost\")\n                    .WithEnvironment(EnvironmentVariables.RedisPort, \"6379\")\n                    .WithEnvironment(EnvironmentVariables.OrleansClusterId, configuration[getConfigKey(EnvironmentVariables.OrleansClusterId)] ?? \"production\")\n                    .WithEnvironment(EnvironmentVariables.OrleansServiceId, configuration[getConfigKey(EnvironmentVariables.OrleansServiceId)] ?? \"grace-prod\")\n                        .WithEnvironment(EnvironmentVariables.GracePubSubSystem, pubSubSystem)\n                    .WithEnvironment(EnvironmentVariables.AzureServiceBusTopic, configuration[getConfigKey(EnvironmentVariables.AzureServiceBusTopic)] ?? \"graceeventstream\")\n                    .WithEnvironment(EnvironmentVariables.AzureServiceBusSubscription, configuration[getConfigKey(EnvironmentVariables.AzureServiceBusSubscription)] ?? \"grace-server\")\n                    .WithEnvironment(EnvironmentVariables.GraceLogDirectory, publishLogDirectory)\n                    .WithEnvironment(EnvironmentVariables.GraceAuthOidcAuthority, configuration[EnvironmentVariables.GraceAuthOidcAuthority])\n                    .WithEnvironment(EnvironmentVariables.GraceAuthOidcAudience, configuration[EnvironmentVariables.GraceAuthOidcAudience])\n                    .WithEnvironment(EnvironmentVariables.GraceAuthOidcCliClientId, configuration[EnvironmentVariables.GraceAuthOidcCliClientId])\n                    .WithHttpEndpoint(targetPort: 5000, name: \"http\")\n                    .WithHttpsEndpoint(targetPort: 5001, name: \"https\")\n                    .AsHttp2Service()\n                    .WithOtlpExporter();\n\n                var forwardedAuthKeys = new List<string>();\n                AddOptionalEnvironment(graceServer, configuration, EnvironmentVariables.GraceAuthOidcAuthority, forwardedAuthKeys);\n                AddOptionalEnvironment(graceServer, configuration, EnvironmentVariables.GraceAuthOidcAudience, forwardedAuthKeys);\n                LogForwardedSettings(\"Grace.Server auth settings\", forwardedAuthKeys);\n\n                Console.WriteLine(\"Grace.Server publish/production environment configured (Azure resources with MI by default).\");\n                Console.WriteLine(\"  - Redis remains local container\");\n                Console.WriteLine($\"  - OTLP endpoint {otlpEndpoint}\");\n            }\n\n            // Build + run with exit logging (normal + error) and elapsed time.\n            using var appHost = builder.Build();\n            var loggerFactory = appHost.Services.GetService(typeof(ILoggerFactory)) as ILoggerFactory\n                                ?? LoggerFactory.Create(lb => lb.AddSimpleConsole());\n            var logger = loggerFactory.CreateLogger(\"Grace.Aspire.AppHost\");\n            var sw = Stopwatch.StartNew();\n\n            try\n            {\n                appHost.Run();\n                sw.Stop();\n                logger.LogInformation(\"Aspire host exited normally. elapsedMs={Elapsed}\", sw.ElapsedMilliseconds);\n            }\n            catch (Exception ex)\n            {\n                sw.Stop();\n                logger.LogError(ex, \"Aspire host terminated with error. elapsedMs={Elapsed}\", sw.ElapsedMilliseconds);\n                Environment.Exit(1);\n            }\n        }\n        catch (Exception ex)\n        {\n            Console.WriteLine($\"Error starting Aspire host: {ex.Message}\");\n            Console.WriteLine(ex.StackTrace);\n            Environment.Exit(1);\n        }\n    }\n\n    private static string? ResolveSetting(IConfiguration configuration, string name)\n    {\n        var value = Environment.GetEnvironmentVariable(name);\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            value = Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User);\n        }\n\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            value = Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Machine);\n        }\n\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            value = configuration[name];\n        }\n\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            var key = Shared::Grace.Shared.Utilities.getConfigKey(name);\n            value = configuration[key];\n        }\n\n        return string.IsNullOrWhiteSpace(value) ? null : value;\n    }\n\n    private static string GetRequiredSetting(IConfiguration configuration, string name)\n    {\n        var value = ResolveSetting(configuration, name);\n        if (!string.IsNullOrWhiteSpace(value))\n        {\n            return value;\n        }\n\n        var key = Shared::Grace.Shared.Utilities.getConfigKey(name);\n        throw new InvalidOperationException(\n            $\"Missing required setting '{name}' (or '{key}') for DebugAzure.\");\n    }\n\n    private static void AddOptionalEnvironment(\n        IResourceBuilder<ProjectResource> resource,\n        IConfiguration configuration,\n        string name,\n        IList<string> forwardedKeys)\n    {\n        var value = ResolveSetting(configuration, name);\n        if (!string.IsNullOrWhiteSpace(value))\n        {\n            resource.WithEnvironment(name, value);\n            forwardedKeys?.Add(name);\n        }\n    }\n\n    private static void LogForwardedSettings(string label, IList<string> forwardedKeys)\n    {\n        if (forwardedKeys is { Count: > 0 })\n        {\n            Console.WriteLine($\"{label}: {string.Join(\", \", forwardedKeys)}.\");\n        }\n        else\n        {\n            Console.WriteLine($\"{label}: none detected.\");\n        }\n    }\n\n    private static void CleanupDockerContainers(IEnumerable<string> containerNames)\n    {\n        foreach (var name in containerNames)\n        {\n            try\n            {\n                var startInfo = new ProcessStartInfo(\"docker\", $\"rm -f {name}\")\n                {\n                    RedirectStandardOutput = true,\n                    RedirectStandardError = true,\n                    UseShellExecute = false,\n                    CreateNoWindow = true\n                };\n\n                using var proc = Process.Start(startInfo);\n                if (proc is null)\n                {\n                    continue;\n                }\n\n                if (!proc.WaitForExit(5000))\n                {\n                    try\n                    {\n                        proc.Kill(true);\n                    }\n                    catch\n                    {\n                        // Ignore cleanup errors to avoid failing test runs.\n                    }\n                }\n            }\n            catch\n            {\n                // Ignore cleanup errors to avoid failing test runs.\n            }\n        }\n    }\n\n    private static int GetAvailableTcpPort()\n    {\n        var listener = new TcpListener(IPAddress.Loopback, 0);\n        listener.Start();\n        var port = ((IPEndPoint)listener.LocalEndpoint).Port;\n        listener.Stop();\n        return port;\n    }\n\n    private static ReferenceExpression BuildCosmosEmulatorConnectionString(\n        IResourceBuilder<AzureCosmosDBResource> cosmos,\n        string accountKey)\n    {\n            if (cosmos.Resource is IResourceWithEndpoints cosmosWithEndpoints)\n            {\n                EndpointReference? selected = null;\n\n            foreach (var endpoint in cosmosWithEndpoints.GetEndpoints())\n            {\n                if (endpoint.EndpointAnnotation.TargetPort == 8081)\n                {\n                    selected = endpoint;\n                    break;\n                }\n\n                if (endpoint.IsHttps)\n                {\n                    selected = endpoint;\n                    break;\n                }\n\n                selected ??= endpoint;\n            }\n\n                if (selected is not null)\n                {\n                    var scheme = selected.EndpointAnnotation.UriScheme;\n                    if (string.IsNullOrWhiteSpace(scheme))\n                    {\n                        scheme = selected.IsHttps ? \"https\" : \"http\";\n                    }\n\n                var hostAndPort = selected.Property(EndpointProperty.HostAndPort);\n                return ReferenceExpression.Create($\"AccountEndpoint={scheme}://{hostAndPort}/;AccountKey={accountKey};\");\n            }\n        }\n\n        return cosmos.Resource.ConnectionStringExpression;\n    }\n\n    private static readonly JsonSerializerOptions jsonOptions = new()\n    {\n        WriteIndented = true,\n        PropertyNamingPolicy = null\n    };\n\n    /// <summary>\n    /// Creates the Service Bus Emulator configuration file with namespace, topics, and subscriptions.\n    /// </summary>\n    private static void CreateServiceBusConfiguration(string configFilePath, IConfiguration configuration)\n    {\n        var topicName = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.AzureServiceBusTopic) ?? \"graceeventstream\";\n        var subscriptionName = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.AzureServiceBusSubscription) ?? \"grace-server\";\n        var testSubscriptionName = $\"{subscriptionName}-tests\";\n\n        var config = new\n        {\n            UserConfig = new\n            {\n                Namespaces = new[]\n                {\n                    new\n                    {\n                        Name = \"sbemulatorns\",\n                        Queues = Array.Empty<object>(),\n                        Topics = new[]\n                        {\n                            new\n                            {\n                                Name = topicName,\n                                Properties = new\n                                {\n                                    DefaultMessageTimeToLive = \"PT1H\",\n                                    DuplicateDetectionHistoryTimeWindow = \"PT20S\",\n                                    RequiresDuplicateDetection = false\n                                },\n                                Subscriptions = new[]\n                                {\n                                    new\n                                    {\n                                        Name = subscriptionName,\n                                        Properties = new\n                                        {\n                                            DeadLetteringOnMessageExpiration = false,\n                                            DefaultMessageTimeToLive = \"PT1H\",\n                                            LockDuration = \"PT1M\",\n                                            MaxDeliveryCount = 10,\n                                            ForwardDeadLetteredMessagesTo = \"\",\n                                            ForwardTo = \"\",\n                                            RequiresSession = false\n                                        },\n                                        Rules = Array.Empty<object>()\n                                    },\n                                    new\n                                    {\n                                        Name = testSubscriptionName,\n                                        Properties = new\n                                        {\n                                            DeadLetteringOnMessageExpiration = false,\n                                            DefaultMessageTimeToLive = \"PT1H\",\n                                            LockDuration = \"PT1M\",\n                                            MaxDeliveryCount = 10,\n                                            ForwardDeadLetteredMessagesTo = \"\",\n                                            ForwardTo = \"\",\n                                            RequiresSession = false\n                                        },\n                                        Rules = Array.Empty<object>()\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                Logging = new\n                {\n                    Type = \"Console\"\n                }\n            }\n        };\n\n        var json = JsonSerializer.Serialize(config, jsonOptions);\n        var existingJson = File.Exists(configFilePath) ? File.ReadAllText(configFilePath) : null;\n        if (existingJson != json)\n        {\n            Console.WriteLine($\"Creating Service Bus Emulator config at {configFilePath}:\\n{json}\");\n            File.WriteAllText(configFilePath, json);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Grace.Aspire.AppHost/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"http://localhost:16196\",\n        \"ASPIRE_ALLOW_UNSECURED_TRANSPORT\": \"true\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"http://localhost:15009\"\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"http://localhost:16196\",\n        \"ASPIRE_ALLOW_UNSECURED_TRANSPORT\": \"true\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:15010\"\n    },\n    \"DebugLocal\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"ASPIRE_RESOURCE_MODE\": \"Local\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"http://localhost:16196\",\n        \"ASPIRE_ALLOW_UNSECURED_TRANSPORT\": \"true\",\n        \"GRACE_TESTING\": \"1\",\n        \"GRACE_TEST_FIXED_PORTS\": \"1\",\n        \"grace__authz__bootstrap__system_admin_users\": \"test-admin\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:15010\"\n    },\n    \"DebugAzure\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"ASPIRE_RESOURCE_MODE\": \"Azure\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"http://localhost:16196\",\n        \"ASPIRE_ALLOW_UNSECURED_TRANSPORT\": \"true\"\n      },\n      \"dotnetRunMessages\": true,\n      \"applicationUrl\": \"https://localhost:15010\"\n    }\n  },\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\"\n}\n"
  },
  {
    "path": "src/Grace.Aspire.AppHost/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\"\n    }\n  },\n  \"Aspire\": {\n    \"Dashboard\": {\n      \"Enabled\": true,\n      \"Port\": 18888,\n      \"OtlpGrpcEndpointUrl\": \"http://localhost:18889\"\n    }\n  }\n}\n\n"
  },
  {
    "path": "src/Grace.Aspire.ServiceDefaults/Extensions.cs",
    "content": "using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.Logging;\nusing OpenTelemetry.Logs;\nusing OpenTelemetry.Metrics;\nusing OpenTelemetry.Trace;\n\nnamespace Microsoft.Extensions.Hosting;\n\npublic static class Extensions\n{\n    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)\n    {\n        builder.ConfigureOpenTelemetry();\n\n        builder.AddDefaultHealthChecks();\n\n        builder.Services.AddServiceDiscovery();\n\n        builder.Services.ConfigureHttpClientDefaults(http =>\n        {\n            // Turn on resilience by default\n            http.AddStandardResilienceHandler();\n\n            // Turn on service discovery by default\n            http.AddServiceDiscovery();\n        });\n\n        return builder;\n    }\n\n    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)\n    {\n        builder.Logging.AddOpenTelemetry(logging =>\n        {\n            logging.IncludeFormattedMessage = true;\n            logging.IncludeScopes = true;\n        });\n\n        builder.Services.AddOpenTelemetry()\n            .WithMetrics(metrics =>\n            {\n                metrics.AddRuntimeInstrumentation()\n                       .AddBuiltInMeters();\n            })\n            .WithTracing(tracing =>\n            {\n                if (builder.Environment.IsDevelopment())\n                {\n                    // We want to view all traces in development\n                    tracing.SetSampler(new AlwaysOnSampler());\n                }\n\n                tracing.AddAspNetCoreInstrumentation()\n                       .AddGrpcClientInstrumentation()\n                       .AddHttpClientInstrumentation();\n            });\n\n        builder.AddOpenTelemetryExporters();\n\n        return builder;\n    }\n\n    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)\n    {\n        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration[\"OTEL_EXPORTER_OTLP_ENDPOINT\"]);\n\n        if (useOtlpExporter)\n        {\n            builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());\n            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());\n            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());\n        }\n\n        // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)\n        // builder.Services.AddOpenTelemetry()\n        //    .WithMetrics(metrics => metrics.AddPrometheusExporter());\n\n        // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)\n        // builder.Services.AddOpenTelemetry()\n        //    .UseAzureMonitor();\n\n        return builder;\n    }\n\n    public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)\n    {\n        builder.Services.AddHealthChecks()\n            // Add a default liveness check to ensure app is responsive\n            .AddCheck(\"self\", () => HealthCheckResult.Healthy(), [\"live\"]);\n\n        return builder;\n    }\n\n    public static WebApplication MapDefaultEndpoints(this WebApplication app)\n    {\n        // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)\n        // app.MapPrometheusScrapingEndpoint();\n\n        // All health checks must pass for app to be considered ready to accept traffic after starting\n        app.MapHealthChecks(\"/health\");\n\n        // Only health checks tagged with the \"live\" tag must pass for app to be considered alive\n        app.MapHealthChecks(\"/alive\", new HealthCheckOptions\n        {\n            Predicate = r => r.Tags.Contains(\"live\")\n        });\n\n        return app;\n    }\n\n    private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) =>\n        meterProviderBuilder.AddMeter(\n            \"Microsoft.AspNetCore.Hosting\",\n            \"Microsoft.AspNetCore.Server.Kestrel\",\n            \"System.Net.Http\");\n}\n"
  },
  {
    "path": "src/Grace.Aspire.ServiceDefaults/Grace.Aspire.ServiceDefaults.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <OutputType>Library</OutputType>\n        <TargetFramework>net10.0</TargetFramework>\n        <LangVersion>preview</LangVersion>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <IsAspireSharedProject>true</IsAspireSharedProject>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n\n        <PackageReference Include=\"Microsoft.Extensions.Http.Resilience\" Version=\"9.10.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.ServiceDiscovery\" Version=\"9.5.2\" />\n        <PackageReference Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" Version=\"1.14.0-rc.1\" />\n        <PackageReference Include=\"OpenTelemetry.Extensions.Hosting\" Version=\"1.14.0-rc.1\" />\n        <PackageReference Include=\"OpenTelemetry.Instrumentation.AspNetCore\" Version=\"1.13.0\" />\n        <PackageReference Include=\"OpenTelemetry.Instrumentation.GrpcNetClient\" Version=\"1.13.0-beta.1\" />\n        <PackageReference Include=\"OpenTelemetry.Instrumentation.Http\" Version=\"1.13.0\" />\n        <PackageReference Include=\"OpenTelemetry.Instrumentation.Runtime\" Version=\"1.13.0\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/AuthorizationSemantics.Tests.fs",
    "content": "namespace Grace.Authorization.Tests\n\nopen FsCheck\nopen Grace.Shared.Authorization\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Reflection\nopen Microsoft.FSharp.Reflection\n\n[<Parallelizable(ParallelScope.All)>]\ntype AuthorizationSemanticsTests() =\n\n    let roleCatalog = RoleCatalog.getAll ()\n\n    let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-1\" }\n    let otherPrincipal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-2\" }\n\n    let ownerId = Guid.Parse(\"11111111-1111-1111-1111-111111111111\")\n    let organizationId = Guid.Parse(\"22222222-2222-2222-2222-222222222222\")\n    let repositoryId = Guid.Parse(\"33333333-3333-3333-3333-333333333333\")\n    let branchId = Guid.Parse(\"44444444-4444-4444-4444-444444444444\")\n\n    let resources =\n        [\n            Resource.System\n            Resource.Owner ownerId\n            Resource.Organization(ownerId, organizationId)\n            Resource.Repository(ownerId, organizationId, repositoryId)\n            Resource.Branch(ownerId, organizationId, repositoryId, branchId)\n            Resource.Path(ownerId, organizationId, repositoryId, \"/docs/readme.md\")\n        ]\n\n    let allOperations =\n        FSharpType.GetUnionCases typeof<Operation>\n        |> Array.map (fun caseInfo -> FSharpValue.MakeUnion(caseInfo, [||]) :?> Operation)\n        |> Array.toList\n\n    let createAssignment scope roleId =\n        {\n            Principal = principal\n            Scope = scope\n            RoleId = roleId\n            Source = \"test\"\n            SourceDetail = None\n            CreatedAt = getCurrentInstant ()\n        }\n\n    let scopeKind scope =\n        match scope with\n        | Scope.System -> \"system\"\n        | Scope.Owner _ -> \"owner\"\n        | Scope.Organization _ -> \"organization\"\n        | Scope.Repository _ -> \"repository\"\n        | Scope.Branch _ -> \"branch\"\n\n    let assertAllowed result =\n        match result with\n        | Allowed _ -> ()\n        | Denied reason -> Assert.Fail($\"Expected Allowed but got Denied: {reason}\")\n\n    let assertDenied result =\n        match result with\n        | Denied _ -> ()\n        | Allowed reason -> Assert.Fail($\"Expected Denied but got Allowed: {reason}\")\n\n    [<Test>]\n    member _.RoleCatalogMatrixMatchesPermissionChecks() =\n        for role in roleCatalog do\n            for resource in resources do\n                let scopes = scopesForResource resource\n\n                for scope in scopes do\n                    for operation in allOperations do\n                        let assignments = [ createAssignment scope role.RoleId ]\n\n                        let result =\n                            checkPermission\n                                roleCatalog\n                                assignments\n                                []\n                                [ principal ]\n                                Set.empty\n                                operation\n                                resource\n\n                        let expectedAllowed =\n                            role.AllowedOperations.Contains operation\n                            && role.AppliesTo.Contains(scopeKind scope)\n\n                        if expectedAllowed then\n                            assertAllowed result\n                        else\n                            assertDenied result\n\n    [<Test>]\n    member _.RepoAdminIncludesBranchAdmin() =\n        let repoAdmin =\n            roleCatalog\n            |> List.find (fun role -> role.RoleId.Equals(\"RepoAdmin\", StringComparison.OrdinalIgnoreCase))\n\n        Assert.That(repoAdmin.AllowedOperations.Contains BranchAdmin, Is.True)\n\n    [<Test>]\n    member _.IrrelevantAssignmentsDoNotAffectDecision() =\n        let property (operation: Operation) =\n            let scope = Scope.Repository(ownerId, organizationId, repositoryId)\n            let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let assignment = createAssignment scope \"RepoAdmin\"\n            let baseResult = checkPermission roleCatalog [ assignment ] [] [ principal ] Set.empty operation resource\n\n            let extraAssignment =\n                {\n                    assignment with\n                        Principal = otherPrincipal\n                }\n\n            let withExtra =\n                checkPermission\n                    roleCatalog\n                    [ extraAssignment; assignment ]\n                    []\n                    [ principal ]\n                    Set.empty\n                    operation\n                    resource\n\n            baseResult = withExtra\n\n        Check.QuickThrowOnFailure property\n\n    [<Test>]\n    member _.ScopeIrrelevanceDoesNotAffectDecision() =\n        let property (operation: Operation) =\n            let scope = Scope.Repository(ownerId, organizationId, repositoryId)\n            let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let assignment = createAssignment scope \"RepoAdmin\"\n            let baseResult = checkPermission roleCatalog [ assignment ] [] [ principal ] Set.empty operation resource\n\n            let unrelatedAssignment =\n                createAssignment (Scope.Branch(ownerId, organizationId, repositoryId, branchId)) \"RepoAdmin\"\n\n            let withUnrelated =\n                checkPermission\n                    roleCatalog\n                    [ unrelatedAssignment; assignment ]\n                    []\n                    [ principal ]\n                    Set.empty\n                    operation\n                    resource\n\n            baseResult = withUnrelated\n\n        Check.QuickThrowOnFailure property\n\n    [<Test>]\n    member _.RoleIdCaseInsensitive() =\n        let property (operation: Operation) =\n            let scope = Scope.Repository(ownerId, organizationId, repositoryId)\n            let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let assignment = createAssignment scope \"RepoReader\"\n            let baseResult = checkPermission roleCatalog [ assignment ] [] [ principal ] Set.empty operation resource\n\n            let mixedCaseAssignment = createAssignment scope \"rEpOrEaDeR\"\n\n            let withMixedCase =\n                checkPermission\n                    roleCatalog\n                    [ mixedCaseAssignment ]\n                    []\n                    [ principal ]\n                    Set.empty\n                    operation\n                    resource\n\n            baseResult = withMixedCase\n\n        Check.QuickThrowOnFailure property\n\n    [<Test>]\n    member _.RbacMonotonicityForNonPathOps() =\n        let property (operation: Operation) =\n            match operation with\n            | PathRead\n            | PathWrite -> true\n            | _ ->\n                let scope = Scope.Repository(ownerId, organizationId, repositoryId)\n                let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n                let baseAssignment = createAssignment scope \"RepoReader\"\n                let extraAssignment = createAssignment scope \"RepoAdmin\"\n\n                let baseResult =\n                    checkPermission roleCatalog [ baseAssignment ] [] [ principal ] Set.empty operation resource\n\n                let withExtra =\n                    checkPermission\n                        roleCatalog\n                        [ extraAssignment; baseAssignment ]\n                        []\n                        [ principal ]\n                        Set.empty\n                        operation\n                        resource\n\n                match baseResult with\n                | Allowed _ ->\n                    match withExtra with\n                    | Allowed _ -> true\n                    | Denied _ -> false\n                | Denied _ -> true\n\n        Check.QuickThrowOnFailure property\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/ClaimMapping.Tests.fs",
    "content": "namespace Grace.Authorization.Tests\n\nopen Grace.Server.Security\nopen Microsoft.AspNetCore.Authentication\nopen Microsoft.Extensions.Logging.Abstractions\nopen NUnit.Framework\nopen System\nopen System.Security.Claims\n\n[<Parallelizable(ParallelScope.All)>]\ntype ClaimMappingTests() =\n\n    let createPrincipal (claims: Claim list) =\n        let identity = ClaimsIdentity(claims, \"Bearer\")\n        ClaimsPrincipal(identity)\n\n    let findValues (claimType: string) (claims: Claim list) =\n        claims\n        |> List.filter (fun claim -> String.Equals(claim.Type, claimType, StringComparison.Ordinal))\n        |> List.map (fun claim -> claim.Value)\n\n    [<Test>]\n    member _.ClaimsTransformationDoesNotDuplicateGraceUserId() =\n        let principal =\n            createPrincipal [ Claim(PrincipalMapper.GraceUserIdClaim, \"existing-user\")\n                              Claim(\"sub\", \"subject-1\") ]\n\n        let transformer = GraceClaimsTransformation(NullLogger<GraceClaimsTransformation>.Instance)\n        let transformed = (transformer :> IClaimsTransformation).TransformAsync(principal).Result\n\n        let graceUserIds =\n            transformed.Claims\n            |> Seq.filter (fun claim -> claim.Type = PrincipalMapper.GraceUserIdClaim)\n            |> Seq.map (fun claim -> claim.Value)\n            |> Seq.toList\n\n        Assert.That(graceUserIds, Is.EquivalentTo([ \"existing-user\" ]))\n\n    [<Test>]\n    member _.ClaimMappingIsIdempotent() =\n        let principal =\n            createPrincipal [ Claim(\"roles\", \"Admin\")\n                              Claim(\"scp\", \"repo.write repo.read\")\n                              Claim(\"groups\", \"group-1\") ]\n\n        let first = ClaimMapping.mapClaims principal\n        let augmented = ClaimsPrincipal(ClaimsIdentity(principal.Claims |> Seq.append first, \"Bearer\"))\n        let second = ClaimMapping.mapClaims augmented\n\n        Assert.That(second, Is.Empty)\n\n    [<Test>]\n    member _.DedupesGraceClaimsAndGroups() =\n        let principal =\n            createPrincipal [ Claim(PrincipalMapper.GraceClaim, \"repo.read\")\n                              Claim(PrincipalMapper.GraceClaim, \"repo.read\")\n                              Claim(PrincipalMapper.GraceGroupIdClaim, \"group-1\")\n                              Claim(PrincipalMapper.GraceGroupIdClaim, \"group-1\")\n                              Claim(\"roles\", \"repo.read\")\n                              Claim(\"groups\", \"group-1\") ]\n\n        let mapped = ClaimMapping.mapClaims principal\n\n        let graceClaims = findValues PrincipalMapper.GraceClaim mapped |> List.sort\n        let graceGroups = findValues PrincipalMapper.GraceGroupIdClaim mapped |> List.sort\n\n        Assert.That(graceClaims, Is.EquivalentTo([]))\n        Assert.That(graceGroups, Is.EquivalentTo([]))\n\n    [<Test>]\n    member _.SplitsScopesOnSpacesWithoutEmptyEntries() =\n        let principal =\n            createPrincipal [ Claim(\"scp\", \"repo.read  repo.write   \")\n                              Claim(\"scope\", \"  repo.list\") ]\n\n        let mapped = ClaimMapping.mapClaims principal\n        let graceClaims = findValues PrincipalMapper.GraceClaim mapped |> List.sort\n\n        Assert.That(graceClaims, Is.EquivalentTo([ \"repo.list\"; \"repo.read\"; \"repo.write\" ]))\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/EndpointAuthorizationManifest.Tests.fs",
    "content": "namespace Grace.Authorization.Tests\n\nopen Grace.Server.Security.EndpointAuthorizationManifest\nopen NUnit.Framework\nopen System\nopen System.IO\nopen System.Text.RegularExpressions\n\n[<Parallelizable(ParallelScope.All)>]\ntype EndpointAuthorizationManifestTests() =\n\n    let startupPath =\n        Path.GetFullPath(Path.Combine(__SOURCE_DIRECTORY__, \"..\", \"Grace.Server\", \"Startup.Server.fs\"))\n\n    let tokenRegex =\n        Regex(\n            \"(?<method>\\\\bGET\\\\b|\\\\bPOST\\\\b|\\\\bPUT\\\\b)\\\\s*\\\\[|(?<kind>subRoute|routef|route)\\\\s+\\\"(?<path>[^\\\"]+)\\\"\",\n            RegexOptions.Multiline\n        )\n\n    let parseStartupRoutes () =\n        let text = File.ReadAllText(startupPath)\n        let matches = tokenRegex.Matches(text)\n        let mutable currentPrefix = String.Empty\n        let mutable currentMethod = String.Empty\n        let routes = ResizeArray<string * string>()\n\n        for matchItem in matches do\n            if matchItem.Groups[\"method\"].Success then\n                currentMethod <- matchItem.Groups[\"method\"].Value\n            elif matchItem.Groups[\"kind\"].Success then\n                let kind = matchItem.Groups[\"kind\"].Value\n                let path = matchItem.Groups[\"path\"].Value\n\n                if kind = \"subRoute\" then\n                    currentPrefix <- path\n                else\n                    if String.IsNullOrWhiteSpace currentMethod then\n                        invalidOp $\"Missing HTTP method before route '{path}'.\"\n                    else\n                        let fullPath =\n                            if String.IsNullOrWhiteSpace currentPrefix then\n                                path\n                            else\n                                $\"{currentPrefix}{path}\"\n\n                        routes.Add(currentMethod, fullPath)\n\n        routes\n        |> Seq.toList\n        |> List.append [ (\"GET\", \"/metrics\"); (\"GET\", \"/notifications\") ]\n\n    [<Test>]\n    member _.ManifestCoversAllRoutes() =\n        let startupRoutes = parseStartupRoutes () |> Set.ofList\n        let manifestRoutes =\n            definitions\n            |> List.map (fun definition -> definition.Method, definition.Path)\n            |> Set.ofList\n\n        let missing = startupRoutes - manifestRoutes\n        if missing.Count > 0 then\n            let missingText =\n                missing\n                |> Seq.sort\n                |> Seq.map (fun (method, path) -> $\"{method} {path}\")\n                |> String.concat Environment.NewLine\n\n            Assert.Fail($\"EndpointAuthorizationManifest is missing routes:{Environment.NewLine}{missingText}\")\n\n        let extra = manifestRoutes - startupRoutes\n        if extra.Count > 0 then\n            let extraText =\n                extra\n                |> Seq.sort\n                |> Seq.map (fun (method, path) -> $\"{method} {path}\")\n                |> String.concat Environment.NewLine\n\n            Assert.Fail($\"EndpointAuthorizationManifest includes routes not in Startup.Server.fs:{Environment.NewLine}{extraText}\")\n\n    [<Test>]\n    member _.ManifestDoesNotContainDuplicates() =\n        let duplicates =\n            definitions\n            |> List.groupBy (fun definition -> definition.Method, definition.Path)\n            |> List.choose (fun (key, entries) ->\n                if entries.Length > 1 then\n                    Some key\n                else\n                    None)\n\n        if duplicates.Length > 0 then\n            let message =\n                duplicates\n                |> List.map (fun (method, path) -> $\"{method} {path}\")\n                |> String.concat Environment.NewLine\n\n            Assert.Fail($\"EndpointAuthorizationManifest contains duplicate entries:{Environment.NewLine}{message}\")\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/Grace.Authorization.Tests.fsproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <LangVersion>preview</LangVersion>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <GenerateProgramFile>false</GenerateProgramFile>\n    <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    <OtherFlags>--test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen</OtherFlags>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"AuthorizationSemantics.Tests.fs\" />\n    <Compile Include=\"ClaimMapping.Tests.fs\" />\n    <Compile Include=\"PathPermissions.Tests.fs\" />\n    <Compile Include=\"PersonalAccessToken.Tests.fs\" />\n    <Compile Include=\"PermissionEvaluator.Tests.fs\" />\n    <Compile Include=\"EndpointAuthorizationManifest.Tests.fs\" />\n    <Compile Include=\"Program.fs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\" />\n    <PackageReference Include=\"FsCheck\" Version=\"3.3.0\" />\n    <PackageReference Include=\"FsCheck.NUnit\" Version=\"3.3.0\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n    <PackageReference Include=\"NUnit\" Version=\"4.4.0\" />\n    <PackageReference Include=\"NUnit.Analyzers\" Version=\"4.11.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"NUnit3TestAdapter\" Version=\"5.2.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Grace.Server\\Grace.Server.fsproj\" />\n    <ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n    <ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/PathPermissions.Tests.fs",
    "content": "namespace Grace.Authorization.Tests\n\nopen Grace.Shared.Authorization\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype PathPermissionsTests() =\n\n    let ownerId = Guid.Parse(\"11111111-1111-1111-1111-111111111111\")\n    let organizationId = Guid.Parse(\"22222222-2222-2222-2222-222222222222\")\n    let repositoryId = Guid.Parse(\"33333333-3333-3333-3333-333333333333\")\n\n    let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-1\" }\n\n    let createAssignment scope roleId =\n        {\n            Principal = principal\n            Scope = scope\n            RoleId = roleId\n            Source = \"test\"\n            SourceDetail = None\n            CreatedAt = getCurrentInstant ()\n        }\n\n    [<Test>]\n    member _.NormalizesPathSeparators() =\n        let permissions = List<ClaimPermission>()\n        permissions.Add({ Claim = \"engineering\"; DirectoryPermission = DirectoryPermission.Modify })\n\n        let pathPermissions = [ { Path = \"/images\"; Permissions = permissions } ]\n        let claims = Set.ofList [ \"engineering\" ]\n\n        let result = checkPathPermission pathPermissions claims \"\\\\images\" PathWrite\n\n        match result with\n        | Some(Allowed _) -> ()\n        | Some(Denied reason) -> Assert.Fail($\"Expected Allowed but got Denied: {reason}\")\n        | None -> Assert.Fail(\"Expected path permission to match normalized path.\")\n\n    [<Test>]\n    member _.WeirdPathsDoNotThrowAndDenyByDefault() =\n        let weirdPaths =\n            [\n                \"\"\n                \" \"\n                \".\"\n                \"..\"\n                \"../x\"\n                \"./x\"\n                \"//\"\n                \"///\"\n                \"/../x\"\n                \"/./x\"\n            ]\n\n        for path in weirdPaths do\n            let resource = Resource.Path(ownerId, organizationId, repositoryId, path)\n\n            let result =\n                checkPermission\n                    (RoleCatalog.getAll ())\n                    []\n                    []\n                    [ principal ]\n                    Set.empty\n                    Operation.PathRead\n                    resource\n\n            match result with\n            | Denied _ -> ()\n            | Allowed reason -> Assert.Fail($\"Expected Denied but got Allowed for '{path}': {reason}\")\n\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/PermissionEvaluator.Tests.fs",
    "content": "namespace Grace.Authorization.Tests\n\nopen Grace.Server.Security\nopen Grace.Shared.Authorization\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Threading.Tasks\n\n[<Parallelizable(ParallelScope.All)>]\ntype PermissionEvaluatorTests() =\n\n    let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-1\" }\n    let ownerId = Guid.Parse(\"11111111-1111-1111-1111-111111111111\")\n    let organizationId = Guid.Parse(\"22222222-2222-2222-2222-222222222222\")\n    let repositoryId = Guid.Parse(\"33333333-3333-3333-3333-333333333333\")\n    let branchId = Guid.Parse(\"44444444-4444-4444-4444-444444444444\")\n\n    let createAssignment scope roleId =\n        {\n            Principal = principal\n            Scope = scope\n            RoleId = roleId\n            Source = \"test\"\n            SourceDetail = None\n            CreatedAt = getCurrentInstant ()\n        }\n\n    let emptyPathPermissions (_repositoryId: RepositoryId, _correlationId: CorrelationId) =\n        Task.FromResult([ ])\n\n    [<Test>]\n    member _.QueriesScopesForResource() =\n        task {\n            let capturedScopes = ResizeArray<Scope>()\n\n            let getAssignmentsForScope (scope: Scope, _correlationId: CorrelationId) =\n                capturedScopes.Add scope\n                Task.FromResult([ ])\n\n            let evaluator = GracePermissionEvaluator(getAssignmentsForScope, emptyPathPermissions)\n            let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let! _ = (evaluator :> IGracePermissionEvaluator).CheckAsync([ principal ], Set.empty, Operation.RepoRead, resource)\n\n            let expected = scopesForResource resource\n            Assert.That(capturedScopes, Is.EquivalentTo(expected))\n        }\n\n    [<Test>]\n    member _.UnionsAssignmentsAcrossScopes() =\n        task {\n            let getAssignmentsForScope (scope: Scope, _correlationId: CorrelationId) =\n                match scope with\n                | Scope.Organization _ -> Task.FromResult([ createAssignment scope \"OrgAdmin\" ])\n                | _ -> Task.FromResult([ ])\n\n            let evaluator = GracePermissionEvaluator(getAssignmentsForScope, emptyPathPermissions)\n            let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let! result = (evaluator :> IGracePermissionEvaluator).CheckAsync([ principal ], Set.empty, Operation.RepoWrite, resource)\n\n            match result with\n            | Allowed _ -> ()\n            | Denied reason -> Assert.Fail($\"Expected Allowed but got Denied: {reason}\")\n        }\n\n    [<Test>]\n    member _.FetchesPathPermissionsOnlyForPathResources() =\n        task {\n            let mutable pathPermissionCalls = 0\n\n            let getPathPermissions (_repositoryId: RepositoryId, _correlationId: CorrelationId) =\n                pathPermissionCalls <- pathPermissionCalls + 1\n                Task.FromResult([ ])\n\n            let evaluator =\n                GracePermissionEvaluator(\n                    (fun _ -> Task.FromResult([ ])),\n                    getPathPermissions\n                )\n\n            let repositoryResource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let! _ = (evaluator :> IGracePermissionEvaluator).CheckAsync([ principal ], Set.empty, Operation.RepoRead, repositoryResource)\n\n            Assert.That(pathPermissionCalls, Is.EqualTo(0))\n\n            let pathResource = Resource.Path(ownerId, organizationId, repositoryId, \"/docs/readme.md\")\n            let! _ = (evaluator :> IGracePermissionEvaluator).CheckAsync([ principal ], Set.empty, Operation.PathRead, pathResource)\n\n            Assert.That(pathPermissionCalls, Is.EqualTo(1))\n        }\n\n    [<Test>]\n    member _.FailsClosedWhenRoleMissing() =\n        task {\n            let getAssignmentsForScope (scope: Scope, _correlationId: CorrelationId) =\n                Task.FromResult([ createAssignment scope \"MissingRole\" ])\n\n            let evaluator = GracePermissionEvaluator(getAssignmentsForScope, emptyPathPermissions)\n            let resource = Resource.Repository(ownerId, organizationId, repositoryId)\n            let! result = (evaluator :> IGracePermissionEvaluator).CheckAsync([ principal ], Set.empty, Operation.RepoRead, resource)\n\n            match result with\n            | Denied _ -> ()\n            | Allowed reason -> Assert.Fail($\"Expected Denied but got Allowed: {reason}\")\n        }\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/PersonalAccessToken.Tests.fs",
    "content": "namespace Grace.Authorization.Tests\n\nopen FsCheck\nopen Grace.Types.PersonalAccessToken\nopen NUnit.Framework\nopen System\n\n[<Parallelizable(ParallelScope.All)>]\ntype PersonalAccessTokenTests() =\n\n    [<Test>]\n    member _.RoundTripParsesFormattedToken() =\n        let userId = \"user-123\"\n        let tokenId = Guid.Parse(\"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\")\n        let secret = Array.init 32 (fun index -> byte (index + 1))\n\n        let token = formatToken userId tokenId secret\n\n        match tryParseToken token with\n        | None -> Assert.Fail(\"Expected token to parse.\")\n        | Some(parsedUserId, parsedTokenId, parsedSecret) ->\n            Assert.That(parsedUserId, Is.EqualTo(userId))\n            Assert.That(parsedTokenId, Is.EqualTo(tokenId))\n            Assert.That(parsedSecret, Is.EquivalentTo(secret))\n\n    [<Test>]\n    member _.TryParseNeverThrows() =\n        let property (input: string) =\n            try\n                tryParseToken input |> ignore\n                true\n            with\n            | _ -> false\n\n        Check.QuickThrowOnFailure property\n\n    [<Test>]\n    member _.RejectsMalformedTokens() =\n        let tokenId = Guid.NewGuid().ToString(\"N\")\n        let goodUser = \"user-123\"\n        let goodSecret = Convert.ToBase64String(Array.init 32 (fun i -> byte (i + 1))).TrimEnd('=').Replace('+', '-').Replace('/', '_')\n        let goodUserB64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(goodUser)).TrimEnd('=').Replace('+', '-').Replace('/', '_')\n\n        let malformed =\n            [\n                \"\"\n                \" \"\n                \"not-a-token\"\n                $\"{TokenPrefix}{goodUserB64}.{tokenId}\" // missing segment\n                $\"{TokenPrefix}{goodUserB64}.{Guid.NewGuid()}.{goodSecret}\" // wrong guid format\n                $\"{TokenPrefix}@@@.{tokenId}.{goodSecret}\" // invalid user b64\n                $\"{TokenPrefix}{goodUserB64}.{tokenId}.@@@\" // invalid secret b64\n                $\"{TokenPrefix}{goodUserB64}.{tokenId}.abcd\" // wrong secret length\n            ]\n\n        for value in malformed do\n            Assert.That(tryParseToken value, Is.EqualTo(None), $\"Expected malformed token to be rejected: {value}\")\n"
  },
  {
    "path": "src/Grace.Authorization.Tests/Program.fs",
    "content": "﻿module Program\n\n[<EntryPoint>]\nlet main _ = 0\n"
  },
  {
    "path": "src/Grace.CLI/AGENTS.md",
    "content": "# Grace.CLI Agents Guide\n\nRead `../AGENTS.md` for global expectations before updating CLI code.\n\n## Purpose\n\n- Provide developer tooling entry points that wrap Grace services via\n  System.CommandLine and Spectre.Console.\n- Surface Grace workflows through friendly commands while keeping logic\n  reusable for tests and automation.\n\n## Key Patterns\n\n- Define command arguments and options in dedicated `Options` modules that\n  produce `Option<'T>` values.\n- Create handlers with `CommandHandler.Create(...)` that delegate to reusable\n  services (often via `Grace.SDK`).\n- Use Spectre.Console for user interaction, but keep core logic testable and\n  separate from console presentation.\n- Maintain alignment with DTOs and contracts defined in `Grace.Types`.\n- Auth commands support PATs via `GRACE_TOKEN` (PAT-only), Auth0 M2M, and\n  Auth0 interactive login with secure token storage. Local token files are\n  disabled. Keep token values out of output except on creation.\n- Avoid positional parameters; prefer named options for clarity.\n\n## Project Rules\n\n1. Keep handlers thin; move heavier logic into services or helpers that are\n   straightforward to unit test.\n2. Preserve existing option names and switches. Introduce new aliases when\n   expanding behavior instead of breaking existing scripts.\n3. Capture new command patterns or usage tips in this document to guide future  \n   agents.\n4. Root and selected subcommand help grouping lives in\n   `src/Grace.CLI/Program.CLI.fs` under `rootHelpSections` and the related\n   `*HelpSections` lists; update those lists when adding or renaming commands\n   so new entries do not silently drift into \"Other\".\n\n## Local State DB\n\n- Local status and object cache are stored in `.grace/grace-local.db`.\n- SQLite side files (`.db-wal`, `.db-shm`, and optional `.db-journal`) are\n  internal; ignore them in repo scans and watch change detection except for\n  status-change coordination.\n\n## Recent Patterns\n\n- `grace history` commands operate without requiring a repo `graceconfig.json`.\n  Avoid `Configuration.Current()` in history-related flows.\n- `grace connect` accepts a positional shortcut in the form\n  `owner/organization/repository`; do not combine it with explicit owner,\n  organization, or repository options.\n\n## Continuous Review Commands\n\n- `grace workitem` (aliases: `work`, `work-item`, `wi`) covers create/show/status,\n  linking references or promotion sets, and attach flows (`summary`, `prompt`, `notes`).\n- `grace review` covers inbox/open/checkpoint/delta/resolve/deepen. Inbox and\n  delta remain CLI stubs until server endpoints land.\n- `grace queue` covers status/enqueue/pause/resume/dequeue; prefer\n  `--branch` but `--branch-id`/`--branch-name` still work.\n\n## Validation\n\n- Add option parsing tests and handler unit tests for new functionality.\n- Manually exercise impacted commands when practical and ensure\n  `dotnet build --configuration Release` stays green.\n\n## Command Modules (`Grace.CLI.Command`)\n\n- Parameter classes usually derive from `ParameterBase()`. Keep them\n  lightweight and validated at construction.\n- Organize command-specific helpers under `Options` modules and wrap\n  invocation with `CommandHandler.Create`.\n- Enforce that command parsing remains thin. Push complex behavior into\n  services so tests can cover edge cases.\n- Add parsing and handler behavior tests in tandem with any new command\n  implementation.\n"
  },
  {
    "path": "src/Grace.CLI/Command/Access.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.Access\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen Spectre.Console\nopen Spectre.Console.Json\nopen System\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule Access =\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let branchId =\n            new Option<BranchId>(\n                OptionName.BranchId,\n                Required = false,\n                Description = \"The branch ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let principalTypeRequired =\n            (new Option<string>(\n                OptionName.PrincipalType,\n                Required = true,\n                Description = \"The principal type (User, Group, Service).\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<PrincipalType> ())\n\n        let principalIdRequired =\n            new Option<string>(OptionName.PrincipalId, Required = true, Description = \"The principal identifier.\", Arity = ArgumentArity.ExactlyOne)\n\n        let principalTypeOptional =\n            (new Option<string>(\n                OptionName.PrincipalType,\n                Required = false,\n                Description = \"Optional principal type filter (User, Group, Service).\",\n                Arity = ArgumentArity.ZeroOrOne\n            ))\n                .AcceptOnlyFromAmong(listCases<PrincipalType> ())\n\n        let principalIdOptional =\n            new Option<string>(OptionName.PrincipalId, Required = false, Description = \"Optional principal identifier filter.\", Arity = ArgumentArity.ZeroOrOne)\n\n        let scopeKindRequired =\n            (new Option<string>(\n                OptionName.ScopeKind,\n                Required = true,\n                Description = \"Scope kind (system, owner, org, repo, branch).\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(\n                    [|\n                        \"system\"\n                        \"owner\"\n                        \"org\"\n                        \"organization\"\n                        \"repo\"\n                        \"repository\"\n                        \"branch\"\n                    |]\n                )\n\n        let roleId = new Option<string>(OptionName.RoleId, Required = true, Description = \"Role identifier.\", Arity = ArgumentArity.ExactlyOne)\n\n        let source = new Option<string>(OptionName.Source, Required = false, Description = \"Optional role assignment source.\", Arity = ArgumentArity.ZeroOrOne)\n\n        let sourceDetail =\n            new Option<string>(\n                OptionName.SourceDetail,\n                Required = false,\n                Description = \"Optional role assignment source detail.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let pathRequired = new Option<string>(OptionName.Path, Required = true, Description = \"Repository relative path.\", Arity = ArgumentArity.ExactlyOne)\n\n        let pathOptional =\n            new Option<string>(OptionName.Path, Required = false, Description = \"Optional repository relative path filter.\", Arity = ArgumentArity.ZeroOrOne)\n\n        let claim =\n            new Option<string []>(\n                OptionName.Claim,\n                Required = true,\n                Description = \"Claim to grant permissions for (repeatable).\",\n                Arity = ArgumentArity.OneOrMore\n            )\n\n        let directoryPermission =\n            new Option<string []>(\n                OptionName.DirectoryPermission,\n                Required = true,\n                Description = \"Directory permission to apply (repeatable; match --claim order).\",\n                Arity = ArgumentArity.OneOrMore\n            )\n\n        let operationRequired =\n            (new Option<string>(OptionName.Operation, Required = true, Description = \"Operation to check.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(listCases<Operation> ())\n\n        let resourceKindRequired =\n            (new Option<string>(\n                OptionName.ResourceKind,\n                Required = true,\n                Description = \"Resource kind (system, owner, org, repo, branch, path).\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(\n                    [|\n                        \"system\"\n                        \"owner\"\n                        \"org\"\n                        \"organization\"\n                        \"repo\"\n                        \"repository\"\n                        \"branch\"\n                        \"path\"\n                    |]\n                )\n\n    let private formatScope (scope: Scope) =\n        match scope with\n        | Scope.System -> \"System\"\n        | Scope.Owner ownerId -> $\"Owner:{ownerId}\"\n        | Scope.Organization (ownerId, organizationId) -> $\"Org:{ownerId}/{organizationId}\"\n        | Scope.Repository (ownerId, organizationId, repositoryId) -> $\"Repo:{ownerId}/{organizationId}/{repositoryId}\"\n        | Scope.Branch (ownerId, organizationId, repositoryId, branchId) -> $\"Branch:{ownerId}/{organizationId}/{repositoryId}/{branchId}\"\n\n    let private formatClaimPermissions (permissions: IEnumerable<ClaimPermission>) =\n        permissions\n        |> Seq.map (fun permission -> $\"{permission.Claim}:{permission.DirectoryPermission}\")\n        |> String.concat \", \"\n\n    let private renderAssignments (parseResult: ParseResult) (assignments: RoleAssignment list) =\n        if parseResult |> hasOutput then\n            if Seq.isEmpty assignments then\n                logToAnsiConsole Colors.Highlighted \"No role assignments found.\"\n            else\n                let table = Table(Border = TableBorder.DoubleEdge)\n\n                table.AddColumns(\n                    [|\n                        TableColumn($\"[{Colors.Important}]Principal[/]\")\n                        TableColumn($\"[{Colors.Important}]Scope[/]\")\n                        TableColumn($\"[{Colors.Important}]Role[/]\")\n                        TableColumn($\"[{Colors.Important}]Source[/]\")\n                        TableColumn($\"[{Colors.Important}]Created[/]\")\n                    |]\n                )\n                |> ignore\n\n                for assignment in assignments do\n                    let principalText = $\"{assignment.Principal.PrincipalType}:{assignment.Principal.PrincipalId}\"\n                    let sourceDetail = assignment.SourceDetail |> Option.defaultValue \"\"\n\n                    let sourceText =\n                        if String.IsNullOrWhiteSpace sourceDetail then\n                            assignment.Source\n                        else\n                            $\"{assignment.Source} ({sourceDetail})\"\n\n                    table.AddRow(\n                        $\"[{Colors.Deemphasized}]{principalText}[/]\",\n                        formatScope assignment.Scope,\n                        assignment.RoleId,\n                        sourceText,\n                        formatInstantExtended assignment.CreatedAt\n                    )\n                    |> ignore\n\n                AnsiConsole.Write(table)\n\n    let private renderRoles (parseResult: ParseResult) (roles: RoleDefinition list) =\n        if parseResult |> hasOutput then\n            if Seq.isEmpty roles then\n                logToAnsiConsole Colors.Highlighted \"No roles found.\"\n            else\n                let table = Table(Border = TableBorder.DoubleEdge)\n\n                table.AddColumns(\n                    [|\n                        TableColumn($\"[{Colors.Important}]Role[/]\")\n                        TableColumn($\"[{Colors.Important}]Applies To[/]\")\n                        TableColumn($\"[{Colors.Important}]Operations[/]\")\n                    |]\n                )\n                |> ignore\n\n                for role in roles do\n                    let appliesTo = role.AppliesTo |> Seq.sort |> String.concat \", \"\n\n                    let operations =\n                        role.AllowedOperations\n                        |> Seq.map string\n                        |> Seq.sort\n                        |> String.concat \", \"\n\n                    table.AddRow($\"[{Colors.Deemphasized}]{role.RoleId}[/]\", appliesTo, operations)\n                    |> ignore\n\n                AnsiConsole.Write(table)\n\n    let private renderPathPermissions (parseResult: ParseResult) (pathPermissions: PathPermission list) =\n        if parseResult |> hasOutput then\n            if Seq.isEmpty pathPermissions then\n                logToAnsiConsole Colors.Highlighted \"No path permissions found.\"\n            else\n                let table = Table(Border = TableBorder.DoubleEdge)\n\n                table.AddColumns(\n                    [|\n                        TableColumn($\"[{Colors.Important}]Path[/]\")\n                        TableColumn($\"[{Colors.Important}]Claims[/]\")\n                    |]\n                )\n                |> ignore\n\n                for pathPermission in pathPermissions do\n                    let claims = formatClaimPermissions pathPermission.Permissions\n\n                    table.AddRow($\"[{Colors.Deemphasized}]{pathPermission.Path}[/]\", claims)\n                    |> ignore\n\n                AnsiConsole.Write(table)\n\n    let private renderPermissionCheck (parseResult: ParseResult) (result: PermissionCheckResult) =\n        if parseResult |> hasOutput then\n            match result with\n            | Allowed reason -> AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Allowed[/]: {Markup.Escape(reason)}\")\n            | Denied reason -> AnsiConsole.MarkupLine($\"[{Colors.Error}]Denied[/]: {Markup.Escape(reason)}\")\n\n    let private validateClaimPermissions (parseResult: ParseResult) =\n        let correlationId = getCorrelationId parseResult\n        let claims = parseResult.GetValue(Options.claim)\n        let permissions = parseResult.GetValue(Options.directoryPermission)\n\n        if isNull claims || claims.Length = 0 then\n            Error(GraceError.Create \"At least one --claim value is required.\" correlationId)\n        elif isNull permissions || permissions.Length = 0 then\n            Error(GraceError.Create \"At least one --dir-perm value is required.\" correlationId)\n        elif claims.Length <> permissions.Length then\n            Error(GraceError.Create \"--claim and --dir-perm counts must match.\" correlationId)\n        else\n            let invalid =\n                permissions\n                |> Array.tryFind (fun value ->\n                    discriminatedUnionFromString<DirectoryPermission> value\n                    |> Option.isNone)\n\n            match invalid with\n            | Some value -> Error(GraceError.Create $\"Invalid DirectoryPermission '{value}'.\" correlationId)\n            | None -> Ok parseResult\n\n    let private validatePrincipalFilter (parseResult: ParseResult) (principalType: string option) (principalId: string option) =\n        let correlationId = getCorrelationId parseResult\n        let principalTypeValue = principalType |> Option.defaultValue String.Empty\n        let principalIdValue = principalId |> Option.defaultValue String.Empty\n\n        if String.IsNullOrWhiteSpace principalTypeValue\n           && String.IsNullOrWhiteSpace principalIdValue then\n            Ok parseResult\n        elif String.IsNullOrWhiteSpace principalTypeValue\n             || String.IsNullOrWhiteSpace principalIdValue then\n            Error(GraceError.Create \"PrincipalType and PrincipalId must be provided together.\" correlationId)\n        else\n            Ok parseResult\n\n    let private validatePathResource (parseResult: ParseResult) (resourceKind: string) (pathValue: string) =\n        if\n            resourceKind.Equals(\"path\", StringComparison.InvariantCultureIgnoreCase)\n            && String.IsNullOrWhiteSpace pathValue\n        then\n            Error(GraceError.Create \"Path is required for Path resources.\" (getCorrelationId parseResult))\n        else\n            Ok parseResult\n\n    type GrantRole() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            GrantRoleParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                BranchId = graceIds.BranchIdString,\n                                PrincipalType = parseResult.GetValue(Options.principalTypeRequired),\n                                PrincipalId = parseResult.GetValue(Options.principalIdRequired),\n                                ScopeKind = parseResult.GetValue(Options.scopeKindRequired),\n                                RoleId = parseResult.GetValue(Options.roleId),\n                                Source = parseResult.GetValue(Options.source),\n                                SourceDetail = parseResult.GetValue(Options.sourceDetail),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Access.GrantRole(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.GrantRole(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderAssignments parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type RevokeRole() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            RevokeRoleParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                BranchId = graceIds.BranchIdString,\n                                PrincipalType = parseResult.GetValue(Options.principalTypeRequired),\n                                PrincipalId = parseResult.GetValue(Options.principalIdRequired),\n                                ScopeKind = parseResult.GetValue(Options.scopeKindRequired),\n                                RoleId = parseResult.GetValue(Options.roleId),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Access.RevokeRole(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.RevokeRole(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderAssignments parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type ListRoleAssignments() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let principalType =\n                        parseResult.GetValue(Options.principalTypeOptional)\n                        |> Option.ofObj\n\n                    let principalId =\n                        parseResult.GetValue(Options.principalIdOptional)\n                        |> Option.ofObj\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> CommonValidations\n                        >>= (fun _ -> validatePrincipalFilter parseResult principalType principalId)\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            ListRoleAssignmentsParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                BranchId = graceIds.BranchIdString,\n                                PrincipalType = (principalType |> Option.defaultValue \"\"),\n                                PrincipalId = (principalId |> Option.defaultValue \"\"),\n                                ScopeKind = parseResult.GetValue(Options.scopeKindRequired),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Access.ListRoleAssignments(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.ListRoleAssignments(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderAssignments parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type UpsertPathPermission() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> CommonValidations\n                        >>= validateClaimPermissions\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let claims = parseResult.GetValue(Options.claim)\n                        let permissions = parseResult.GetValue(Options.directoryPermission)\n                        let claimPermissions = List<ClaimPermissionParameters>()\n\n                        for index in 0 .. claims.Length - 1 do\n                            claimPermissions.Add(ClaimPermissionParameters(Claim = claims[index], DirectoryPermission = permissions[index]))\n\n                        let parameters =\n                            UpsertPathPermissionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                Path = parseResult.GetValue(Options.pathRequired),\n                                ClaimPermissions = claimPermissions,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Access.UpsertPathPermission(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.UpsertPathPermission(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderPathPermissions parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type RemovePathPermission() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            RemovePathPermissionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                Path = parseResult.GetValue(Options.pathRequired),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Access.RemovePathPermission(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.RemovePathPermission(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderPathPermissions parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type ListPathPermissions() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            ListPathPermissionsParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                Path =\n                                    (parseResult.GetValue(Options.pathOptional)\n                                     |> Option.ofObj\n                                     |> Option.defaultValue \"\"),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Access.ListPathPermissions(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.ListPathPermissions(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderPathPermissions parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type CheckPermission() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let resourceKind = parseResult.GetValue(Options.resourceKindRequired)\n\n                    let pathValue =\n                        parseResult.GetValue(Options.pathOptional)\n                        |> Option.ofObj\n                        |> Option.defaultValue \"\"\n\n                    let principalType =\n                        parseResult.GetValue(Options.principalTypeOptional)\n                        |> Option.ofObj\n\n                    let principalId =\n                        parseResult.GetValue(Options.principalIdOptional)\n                        |> Option.ofObj\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> CommonValidations\n                        >>= (fun _ -> validatePrincipalFilter parseResult principalType principalId)\n                        >>= (fun _ -> validatePathResource parseResult resourceKind pathValue)\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            CheckPermissionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                BranchId = graceIds.BranchIdString,\n                                Operation = parseResult.GetValue(Options.operationRequired),\n                                ResourceKind = resourceKind,\n                                Path = pathValue,\n                                PrincipalType = (principalType |> Option.defaultValue \"\"),\n                                PrincipalId = (principalId |> Option.defaultValue \"\"),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Checking permission.[/]\")\n                                            let! response = Access.CheckPermission(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Access.CheckPermission(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            renderPermissionCheck parseResult graceReturnValue.ReturnValue\n                            return result |> renderOutput parseResult\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    type ListRoles() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let parameters = CommonParameters(CorrelationId = getCorrelationId parseResult)\n\n                    let! result =\n                        if parseResult |> hasOutput then\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                        let! response = Access.ListRoles(parameters)\n                                        t0.Increment(100.0)\n                                        return response\n                                    })\n                        else\n                            Access.ListRoles(parameters)\n\n                    match result with\n                    | Ok graceReturnValue ->\n                        renderRoles parseResult graceReturnValue.ReturnValue\n                        return result |> renderOutput parseResult\n                    | Error error ->\n                        logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                        return result |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    let Build =\n        let addScopeOptions (command: Command) =\n            command\n            |> addOption Options.ownerId\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryId\n            |> addOption Options.branchId\n\n        let accessCommand = new Command(\"access\", Description = \"Manages access control and permissions.\")\n\n        let grantRoleCommand =\n            new Command(\"grant-role\", Description = \"Grants a role to a principal at a scope.\")\n            |> addScopeOptions\n            |> addOption Options.scopeKindRequired\n            |> addOption Options.principalTypeRequired\n            |> addOption Options.principalIdRequired\n            |> addOption Options.roleId\n            |> addOption Options.source\n            |> addOption Options.sourceDetail\n\n        grantRoleCommand.Action <- new GrantRole()\n        accessCommand.Subcommands.Add(grantRoleCommand)\n\n        let revokeRoleCommand =\n            new Command(\"revoke-role\", Description = \"Revokes a role from a principal at a scope.\")\n            |> addScopeOptions\n            |> addOption Options.scopeKindRequired\n            |> addOption Options.principalTypeRequired\n            |> addOption Options.principalIdRequired\n            |> addOption Options.roleId\n\n        revokeRoleCommand.Action <- new RevokeRole()\n        accessCommand.Subcommands.Add(revokeRoleCommand)\n\n        let listRoleAssignmentsCommand =\n            new Command(\"list-role-assignments\", Description = \"Lists role assignments at a scope.\")\n            |> addScopeOptions\n            |> addOption Options.scopeKindRequired\n            |> addOption Options.principalTypeOptional\n            |> addOption Options.principalIdOptional\n\n        listRoleAssignmentsCommand.Action <- new ListRoleAssignments()\n        accessCommand.Subcommands.Add(listRoleAssignmentsCommand)\n\n        let upsertPathPermissionCommand =\n            new Command(\"upsert-path-permission\", Description = \"Upserts repository path permissions.\")\n            |> addScopeOptions\n            |> addOption Options.pathRequired\n            |> addOption Options.claim\n            |> addOption Options.directoryPermission\n\n        upsertPathPermissionCommand.Action <- new UpsertPathPermission()\n        accessCommand.Subcommands.Add(upsertPathPermissionCommand)\n\n        let removePathPermissionCommand =\n            new Command(\"remove-path-permission\", Description = \"Removes repository path permissions.\")\n            |> addScopeOptions\n            |> addOption Options.pathRequired\n\n        removePathPermissionCommand.Action <- new RemovePathPermission()\n        accessCommand.Subcommands.Add(removePathPermissionCommand)\n\n        let listPathPermissionsCommand =\n            new Command(\"list-path-permissions\", Description = \"Lists repository path permissions.\")\n            |> addScopeOptions\n            |> addOption Options.pathOptional\n\n        listPathPermissionsCommand.Action <- new ListPathPermissions()\n        accessCommand.Subcommands.Add(listPathPermissionsCommand)\n\n        let checkPermissionCommand =\n            new Command(\"check\", Description = \"Checks a permission for the current or specified principal.\")\n            |> addScopeOptions\n            |> addOption Options.operationRequired\n            |> addOption Options.resourceKindRequired\n            |> addOption Options.pathOptional\n            |> addOption Options.principalTypeOptional\n            |> addOption Options.principalIdOptional\n\n        checkPermissionCommand.Action <- new CheckPermission()\n        accessCommand.Subcommands.Add(checkPermissionCommand)\n\n        let listRolesCommand = new Command(\"list-roles\", Description = \"Lists available roles.\")\n\n        listRolesCommand.Action <- new ListRoles()\n        accessCommand.Subcommands.Add(listRolesCommand)\n\n        accessCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Admin.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK.Admin\nopen Grace.Shared\nopen Grace.Shared.Parameters\nopen Grace.Shared.Parameters.Reminder\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Validation.Errors\nopen NodaTime\nopen Spectre.Console\nopen Spectre.Console.Json\nopen System\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Linq\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule Admin =\n\n    module Reminder =\n\n        module private Options =\n            let ownerId =\n                new Option<OwnerId>(\n                    OptionName.OwnerId,\n                    Required = false,\n                    Description = \"The repository's owner ID <Guid>.\",\n                    Arity = ArgumentArity.ZeroOrOne,\n                    DefaultValueFactory = (fun _ -> OwnerId.Empty)\n                )\n\n            let ownerName =\n                new Option<String>(\n                    OptionName.OwnerName,\n                    Required = false,\n                    Description = \"The repository's owner name. [default: current owner]\",\n                    Arity = ArgumentArity.ExactlyOne\n                )\n\n            let organizationId =\n                new Option<OrganizationId>(\n                    OptionName.OrganizationId,\n                    Required = false,\n                    Description = \"The repository's organization ID <Guid>.\",\n                    Arity = ArgumentArity.ZeroOrOne,\n                    DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n                )\n\n            let organizationName =\n                new Option<String>(\n                    OptionName.OrganizationName,\n                    Required = false,\n                    Description = \"The repository's organization name. [default: current organization]\",\n                    Arity = ArgumentArity.ZeroOrOne\n                )\n\n            let repositoryId =\n                new Option<RepositoryId>(\n                    OptionName.RepositoryId,\n                    [| \"-r\" |],\n                    Required = false,\n                    Description = \"The repository's ID <Guid>.\",\n                    Arity = ArgumentArity.ExactlyOne,\n                    DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n                )\n\n            let repositoryName =\n                new Option<String>(\n                    OptionName.RepositoryName,\n                    [| \"-n\" |],\n                    Required = false,\n                    Description = \"The name of the repository. [default: current repository]\",\n                    Arity = ArgumentArity.ExactlyOne\n                )\n\n            let reminderId =\n                new Option<String>(\"--reminder-id\", Required = true, Description = \"The ID of the reminder <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n            let maxCount =\n                new Option<int>(\n                    \"--max-count\",\n                    Required = false,\n                    Description = $\"Maximum number of reminders to return. [default: {Constants.DefaultReminderMaxCount}]\",\n                    Arity = ArgumentArity.ExactlyOne,\n                    DefaultValueFactory = (fun _ -> Constants.DefaultReminderMaxCount)\n                )\n\n            let reminderType =\n                (new Option<String>(\n                    \"--reminder-type\",\n                    Required = false,\n                    Description = \"Filter by reminder type (Maintenance, PhysicalDeletion, DeleteCachedState, DeleteZipFile).\",\n                    Arity = ArgumentArity.ZeroOrOne\n                ))\n                    .AcceptOnlyFromAmong(listCases<ReminderTypes> ())\n\n            let actorName =\n                new Option<String>(\n                    \"--actor-name\",\n                    Required = false,\n                    Description = \"Filter by target actor name (e.g., Branch, Repository, Owner).\",\n                    Arity = ArgumentArity.ZeroOrOne\n                )\n\n            let actorId = new Option<String>(\"--actor-id\", Required = true, Description = \"The target actor ID.\", Arity = ArgumentArity.ExactlyOne)\n\n            let dueAfter =\n                new Option<String>(\n                    \"--due-after\",\n                    Required = false,\n                    Description = \"Filter by reminders due after this time (ISO8601).\",\n                    Arity = ArgumentArity.ZeroOrOne\n                )\n\n            let dueBefore =\n                new Option<String>(\n                    \"--due-before\",\n                    Required = false,\n                    Description = \"Filter by reminders due before this time (ISO8601).\",\n                    Arity = ArgumentArity.ZeroOrOne\n                )\n\n            let fireAt =\n                new Option<String>(\n                    \"--fire-at\",\n                    Required = true,\n                    Description = \"When the reminder should fire (ISO8601 format).\",\n                    Arity = ArgumentArity.ExactlyOne\n                )\n\n            let afterDuration =\n                new Option<String>(\n                    \"--after\",\n                    Required = true,\n                    Description = \"Duration to add relative to now (e.g., +15m, +1h, +1d).\",\n                    Arity = ArgumentArity.ExactlyOne\n                )\n\n            let stateJson =\n                new Option<String>(\n                    \"--state-json\",\n                    Required = false,\n                    Description = \"Optional JSON payload for the reminder state.\",\n                    Arity = ArgumentArity.ZeroOrOne\n                )\n\n        let private listRemindersWithProgress (parameters: ListRemindersParameters) =\n            progress\n                .Columns(progressColumns)\n                .StartAsync(fun progressContext ->\n                    task {\n                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                        let! response = Reminder.List(parameters)\n                        t0.Increment(100.0)\n                        return response\n                    })\n\n        // List subcommand\n        type List() =\n            inherit AsynchronousCommandLineAction()\n\n            let listRemindersImpl (parseResult: ParseResult) : Tasks.Task<int> =\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters =\n                    parseResult\n                    |> Grace.CLI.Common.Validations.CommonValidations\n\n                match validateIncomingParameters with\n                | Error error ->\n                    Task.FromResult(\n                        GraceResult.Error error\n                        |> renderOutput parseResult\n                    )\n                | Ok _ ->\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let parameters =\n                        ListRemindersParameters(\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            MaxCount = parseResult.GetValue(Options.maxCount),\n                            ReminderType =\n                                (parseResult.GetValue(Options.reminderType)\n                                 |> Option.ofObj\n                                 |> Option.defaultValue \"\"),\n                            ActorName =\n                                (parseResult.GetValue(Options.actorName)\n                                 |> Option.ofObj\n                                 |> Option.defaultValue \"\"),\n                            DueAfter =\n                                (parseResult.GetValue(Options.dueAfter)\n                                 |> Option.ofObj\n                                 |> Option.defaultValue \"\"),\n                            DueBefore =\n                                (parseResult.GetValue(Options.dueBefore)\n                                 |> Option.ofObj\n                                 |> Option.defaultValue \"\"),\n                            CorrelationId = getCorrelationId parseResult\n                        )\n\n                    task {\n                        let! result =\n                            if parseResult |> hasOutput then\n                                listRemindersWithProgress parameters\n                            else\n                                Reminder.List(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            if parseResult |> hasOutput then\n                                let reminders = graceReturnValue.ReturnValue\n\n                                if Seq.isEmpty reminders then\n                                    logToAnsiConsole Colors.Highlighted \"No reminders found.\"\n                                else\n                                    let table = Table(Border = TableBorder.DoubleEdge)\n\n                                    table.AddColumns(\n                                        [|\n                                            TableColumn($\"[{Colors.Important}]Reminder ID[/]\")\n                                            TableColumn($\"[{Colors.Important}]Type[/]\")\n                                            TableColumn($\"[{Colors.Important}]Actor[/]\")\n                                            TableColumn($\"[{Colors.Important}]Fire Time[/]\")\n                                            TableColumn($\"[{Colors.Important}]Created At[/]\")\n                                        |]\n                                    )\n                                    |> ignore\n\n                                    reminders\n                                    |> Seq.iter (fun reminder ->\n                                        let actorId =\n                                            if reminder.ActorId.Length > 8 then\n                                                reminder.ActorId.Substring(0, 8)\n                                            else\n                                                reminder.ActorId\n\n                                        table.AddRow(\n                                            $\"{reminder.ReminderId}\",\n                                            $\"{reminder.ReminderType}\",\n                                            $\"{actorId}\",\n                                            $\"{reminder.ReminderTime}\",\n                                            $\"{reminder.CreatedAt}\"\n                                        )\n                                        |> ignore)\n\n                                    AnsiConsole.Write(table)\n                        | Error _ -> ()\n\n                        return result |> renderOutput parseResult\n                    }\n\n            override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n                task {\n                    try\n                        return! listRemindersImpl parseResult\n                    with\n                    | ex ->\n                        return\n                            renderOutput\n                                parseResult\n                                (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n                }\n\n        // Get subcommand\n        type Get() =\n            inherit AsynchronousCommandLineAction()\n\n            override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n                task {\n                    try\n                        if parseResult |> verbose then printParseResult parseResult\n\n                        let validateIncomingParameters =\n                            parseResult\n                            |> Grace.CLI.Common.Validations.CommonValidations\n\n                        match validateIncomingParameters with\n                        | Ok _ ->\n                            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                            let parameters =\n                                GetReminderParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    ReminderId = parseResult.GetValue(Options.reminderId),\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            let! result =\n                                if parseResult |> hasOutput then\n                                    progress\n                                        .Columns(progressColumns)\n                                        .StartAsync(fun progressContext ->\n                                            task {\n                                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                                let! response = Reminder.Get(parameters)\n                                                t0.Increment(100.0)\n                                                return response\n                                            })\n                                else\n                                    Reminder.Get(parameters)\n\n                            match result with\n                            | Ok graceReturnValue ->\n                                let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                                AnsiConsole.Write(jsonText)\n                                AnsiConsole.WriteLine()\n                                return Ok graceReturnValue |> renderOutput parseResult\n                            | Error graceError ->\n                                logToAnsiConsole Colors.Error (Markup.Escape($\"{graceError}\"))\n                                return result |> renderOutput parseResult\n                        | Error error -> return Error error |> renderOutput parseResult\n\n                    with\n                    | ex ->\n                        return\n                            renderOutput\n                                parseResult\n                                (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n                }\n\n        // Delete subcommand\n        type Delete() =\n            inherit AsynchronousCommandLineAction()\n\n            override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n                task {\n                    try\n                        if parseResult |> verbose then printParseResult parseResult\n\n                        let validateIncomingParameters =\n                            parseResult\n                            |> Grace.CLI.Common.Validations.CommonValidations\n\n                        match validateIncomingParameters with\n                        | Ok _ ->\n                            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                            let parameters =\n                                DeleteReminderParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    ReminderId = parseResult.GetValue(Options.reminderId),\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            let! result =\n                                if parseResult |> hasOutput then\n                                    progress\n                                        .Columns(progressColumns)\n                                        .StartAsync(fun progressContext ->\n                                            task {\n                                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                                let! response = Reminder.Delete(parameters)\n                                                t0.Increment(100.0)\n                                                return response\n                                            })\n                                else\n                                    Reminder.Delete(parameters)\n\n                            return result |> renderOutput parseResult\n                        | Error error -> return Error error |> renderOutput parseResult\n\n                    with\n                    | ex ->\n                        return\n                            renderOutput\n                                parseResult\n                                (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n                }\n\n        // UpdateTime subcommand\n        type UpdateTime() =\n            inherit AsynchronousCommandLineAction()\n\n            override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n                task {\n                    try\n                        if parseResult |> verbose then printParseResult parseResult\n\n                        let validateIncomingParameters =\n                            parseResult\n                            |> Grace.CLI.Common.Validations.CommonValidations\n\n                        match validateIncomingParameters with\n                        | Ok _ ->\n                            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                            let parameters =\n                                UpdateReminderTimeParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    ReminderId = parseResult.GetValue(Options.reminderId),\n                                    FireAt = parseResult.GetValue(Options.fireAt),\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            let! result =\n                                if parseResult |> hasOutput then\n                                    progress\n                                        .Columns(progressColumns)\n                                        .StartAsync(fun progressContext ->\n                                            task {\n                                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                                let! response = Reminder.UpdateTime(parameters)\n                                                t0.Increment(100.0)\n                                                return response\n                                            })\n                                else\n                                    Reminder.UpdateTime(parameters)\n\n                            return result |> renderOutput parseResult\n                        | Error error -> return Error error |> renderOutput parseResult\n\n                    with\n                    | ex ->\n                        return\n                            renderOutput\n                                parseResult\n                                (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n                }\n\n        // Reschedule subcommand\n        type Reschedule() =\n            inherit AsynchronousCommandLineAction()\n\n            override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n                task {\n                    try\n                        if parseResult |> verbose then printParseResult parseResult\n\n                        let validateIncomingParameters =\n                            parseResult\n                            |> Grace.CLI.Common.Validations.CommonValidations\n\n                        match validateIncomingParameters with\n                        | Ok _ ->\n                            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                            let parameters =\n                                RescheduleReminderParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    ReminderId = parseResult.GetValue(Options.reminderId),\n                                    After = parseResult.GetValue(Options.afterDuration),\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            let! result =\n                                if parseResult |> hasOutput then\n                                    progress\n                                        .Columns(progressColumns)\n                                        .StartAsync(fun progressContext ->\n                                            task {\n                                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                                let! response = Reminder.Reschedule(parameters)\n                                                t0.Increment(100.0)\n                                                return response\n                                            })\n                                else\n                                    Reminder.Reschedule(parameters)\n\n                            return result |> renderOutput parseResult\n                        | Error error -> return Error error |> renderOutput parseResult\n\n                    with\n                    | ex ->\n                        return\n                            renderOutput\n                                parseResult\n                                (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n                }\n\n        // Create subcommand\n        type Create() =\n            inherit AsynchronousCommandLineAction()\n\n            override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n                task {\n                    try\n                        if parseResult |> verbose then printParseResult parseResult\n\n                        let validateIncomingParameters =\n                            parseResult\n                            |> Grace.CLI.Common.Validations.CommonValidations\n\n                        match validateIncomingParameters with\n                        | Ok _ ->\n                            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                            let parameters =\n                                CreateReminderParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    ActorName = parseResult.GetValue(Options.actorName),\n                                    ActorId = parseResult.GetValue(Options.actorId),\n                                    ReminderType = parseResult.GetValue(Options.reminderType),\n                                    FireAt = parseResult.GetValue(Options.fireAt),\n                                    StateJson =\n                                        (parseResult.GetValue(Options.stateJson)\n                                         |> Option.ofObj\n                                         |> Option.defaultValue \"\"),\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            let! result =\n                                if parseResult |> hasOutput then\n                                    progress\n                                        .Columns(progressColumns)\n                                        .StartAsync(fun progressContext ->\n                                            task {\n                                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                                let! response = Reminder.Create(parameters)\n                                                t0.Increment(100.0)\n                                                return response\n                                            })\n                                else\n                                    Reminder.Create(parameters)\n\n                            return result |> renderOutput parseResult\n                        | Error error -> return Error error |> renderOutput parseResult\n\n                    with\n                    | ex ->\n                        return\n                            renderOutput\n                                parseResult\n                                (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n                }\n\n        /// Builds the Reminder subcommand.\n        let Build =\n            let addCommonOptions (command: Command) =\n                command\n                |> addOption Options.ownerName\n                |> addOption Options.ownerId\n                |> addOption Options.organizationName\n                |> addOption Options.organizationId\n                |> addOption Options.repositoryId\n                |> addOption Options.repositoryName\n\n            // Create main command and aliases\n            let reminderCommand = new Command(\"reminder\", Description = \"Administrative commands for managing Grace reminders.\")\n\n            // List subcommand\n            let listCommand =\n                new Command(\"list\", Description = \"List reminders for the repository.\")\n                |> addCommonOptions\n                |> addOption Options.maxCount\n                |> addOption Options.reminderType\n                |> addOption Options.actorName\n                |> addOption Options.dueAfter\n                |> addOption Options.dueBefore\n\n            listCommand.Aliases.Add(\"ls\")\n            listCommand.Action <- new List()\n            reminderCommand.Subcommands.Add(listCommand)\n\n            // Get subcommand\n            let getCommand =\n                new Command(\"get\", Description = \"Get details of a specific reminder.\")\n                |> addCommonOptions\n                |> addOption Options.reminderId\n\n            getCommand.Action <- new Get()\n            reminderCommand.Subcommands.Add(getCommand)\n\n            // Delete subcommand\n            let deleteCommand =\n                new Command(\"delete\", Description = \"Delete a reminder.\")\n                |> addCommonOptions\n                |> addOption Options.reminderId\n\n            deleteCommand.Action <- new Delete()\n            reminderCommand.Subcommands.Add(deleteCommand)\n\n            // UpdateTime subcommand\n            let updateTimeCommand =\n                new Command(\"update-time\", Description = \"Update the fire time for a reminder.\")\n                |> addCommonOptions\n                |> addOption Options.reminderId\n                |> addOption Options.fireAt\n\n            updateTimeCommand.Action <- new UpdateTime()\n            reminderCommand.Subcommands.Add(updateTimeCommand)\n\n            // Reschedule subcommand\n            let rescheduleCommand =\n                new Command(\"reschedule\", Description = \"Reschedule a reminder relative to now.\")\n                |> addCommonOptions\n                |> addOption Options.reminderId\n                |> addOption Options.afterDuration\n\n            rescheduleCommand.Action <- new Reschedule()\n            reminderCommand.Subcommands.Add(rescheduleCommand)\n\n            // Create subcommand\n            let createActorNameOption =\n                new Option<String>(\n                    \"--actor-name\",\n                    Required = true,\n                    Description = \"The target actor name (e.g., Branch, Repository, Owner).\",\n                    Arity = ArgumentArity.ExactlyOne\n                )\n\n            let createReminderTypeOption =\n                (new Option<String>(\n                    \"--reminder-type\",\n                    Required = true,\n                    Description = \"The type of reminder (Maintenance, PhysicalDeletion, DeleteCachedState, DeleteZipFile).\",\n                    Arity = ArgumentArity.ExactlyOne\n                ))\n                    .AcceptOnlyFromAmong(listCases<ReminderTypes> ())\n\n            let createCommand =\n                new Command(\"create\", Description = \"Create a new manual reminder.\")\n                |> addCommonOptions\n                |> addOption createActorNameOption\n                |> addOption Options.actorId\n                |> addOption createReminderTypeOption\n                |> addOption Options.fireAt\n                |> addOption Options.stateJson\n\n            createCommand.Action <- new Create()\n            reminderCommand.Subcommands.Add(createCommand)\n\n            reminderCommand\n\n    /// Builds the Admin subcommand.\n    let Build =\n        // Create main command and aliases\n        let adminCommand = new Command(\"admin\", Description = \"Administrative commands for managing Grace.\")\n        adminCommand.Add(Reminder.Build) |> ignore\n        adminCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Agent.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Automation\nopen Grace.Types.Types\nopen Spectre.Console\nopen System\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.IO\nopen System.Security.Cryptography\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule AgentCommand =\n    module private Options =\n        let addSummaryWorkItemId =\n            new Option<string>(\n                \"--work-item-id\",\n                [| \"--workItemId\"; \"-w\" |],\n                Required = true,\n                Description = \"The work item ID <Guid> or work item number <positive integer>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let startWorkItemId =\n            new Option<string>(\n                \"--work-item-id\",\n                [| \"--workItemId\"; \"-w\" |],\n                Required = true,\n                Description = \"The work item ID <Guid> or work item number <positive integer>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let optionalWorkItemId =\n            new Option<string>(\n                \"--work-item-id\",\n                [| \"--workItemId\"; \"-w\" |],\n                Required = false,\n                Description = \"Optional work item ID <Guid> or work item number <positive integer>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let summaryFile =\n            new Option<string>(\n                \"--summary-file\",\n                [| \"--summaryFile\" |],\n                Required = true,\n                Description = \"Path to the summary file to upload.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let promptFile =\n            new Option<string>(\n                \"--prompt-file\",\n                [| \"--promptFile\" |],\n                Required = false,\n                Description = \"Optional path to the prompt file to upload.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let promptOrigin =\n            new Option<string>(\n                \"--prompt-origin\",\n                [| \"--promptOrigin\" |],\n                Required = false,\n                Description = \"Optional prompt origin metadata.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let addSummaryPromotionSetId =\n            new Option<string>(\n                \"--promotion-set-id\",\n                [| \"--promotion-set\" |],\n                Required = false,\n                Description = \"Optional promotion set ID <Guid> to link as part of add-summary.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let agentId =\n            new Option<string>(\"--agent-id\", [| \"--agentId\" |], Required = true, Description = \"The agent ID <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n        let displayName =\n            new Option<string>(\n                \"--display-name\",\n                [| \"--displayName\" |],\n                Required = true,\n                Description = \"The agent display name.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let source =\n            new Option<string>(\n                OptionName.Source,\n                Required = false,\n                Description = \"The source identifier for this agent session. [default: cli]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> \"cli\")\n            )\n\n        let promotionSetId =\n            new Option<string>(\n                \"--promotion-set-id\",\n                [| \"--promotion-set\" |],\n                Required = false,\n                Description = \"Optional promotion set ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let sessionId =\n            new Option<string>(\n                \"--session-id\",\n                [| \"--sessionId\" |],\n                Required = false,\n                Description = \"Optional session ID to stop or inspect.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let stopReason =\n            new Option<string>(\n                \"--reason\",\n                [| \"--stop-reason\"; \"--stopReason\" |],\n                Required = false,\n                Description = \"Optional reason to record when stopping work.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let operationId =\n            new Option<string>(\n                \"--operation-id\",\n                [| \"--operationId\" |],\n                Required = false,\n                Description = \"Optional idempotency token for deterministic replay.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The organization's name. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                Required = false,\n                Description = \"The repository's name. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    type private AddSummaryResult = { WorkItem: string; SummaryArtifactId: string; PromptArtifactId: string option; PromotionSetId: string option }\n\n    type private LocalAgentSessionState =\n        {\n            AgentId: string\n            AgentDisplayName: string\n            Source: string\n            ActiveSessionId: string\n            ActiveWorkItemIdOrNumber: string\n            ActivePromotionSetId: string\n            LastOperationId: string\n            LastCorrelationId: string\n            LastUpdatedAtUtc: DateTime\n        }\n\n    let private localAgentSessionStateDefault =\n        {\n            AgentId = String.Empty\n            AgentDisplayName = String.Empty\n            Source = \"cli\"\n            ActiveSessionId = String.Empty\n            ActiveWorkItemIdOrNumber = String.Empty\n            ActivePromotionSetId = String.Empty\n            LastOperationId = String.Empty\n            LastCorrelationId = String.Empty\n            LastUpdatedAtUtc = DateTime.MinValue\n        }\n\n    let private localStateFileName = \"agent-session-state.json\"\n\n    let private tryParseGuid (value: string) (errorMessage: string) (parseResult: ParseResult) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value)\n           || Guid.TryParse(value, &parsed) = false\n           || parsed = Guid.Empty then\n            Error(GraceError.Create errorMessage (getCorrelationId parseResult))\n        else\n            Ok parsed\n\n    let private tryNormalizeWorkItemIdentifier (value: string) (parseResult: ParseResult) =\n        let mutable parsedGuid = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value) then\n            Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemId) (getCorrelationId parseResult))\n        elif\n            Guid.TryParse(value, &parsedGuid)\n            && parsedGuid <> Guid.Empty\n        then\n            Ok(parsedGuid.ToString())\n        else\n            let mutable parsedNumber = 0L\n\n            if Int64.TryParse(value, &parsedNumber) then\n                if parsedNumber > 0L then\n                    Ok(parsedNumber.ToString())\n                else\n                    Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemNumber) (getCorrelationId parseResult))\n            else\n                Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemId) (getCorrelationId parseResult))\n\n    let private tryGetOptionString (parseResult: ParseResult) (option: Option<string>) =\n        parseResult.GetValue(option)\n        |> Option.ofObj\n        |> Option.map (fun value -> value.Trim())\n        |> Option.filter (fun value -> not <| String.IsNullOrWhiteSpace(value))\n\n    let private hasRepositoryContextOption (parseResult: ParseResult) =\n        [\n            OptionName.OwnerId\n            OptionName.OwnerName\n            OptionName.OrganizationId\n            OptionName.OrganizationName\n            OptionName.RepositoryId\n            OptionName.RepositoryName\n        ]\n        |> List.exists (isOptionPresent parseResult)\n\n    let private ensureRepositoryContextIsAvailable (parseResult: ParseResult) =\n        if configurationFileExists ()\n           || hasRepositoryContextOption parseResult then\n            Ok()\n        else\n            Error(\n                GraceError.Create\n                    \"No Grace repository configuration was found. Run `grace config write` first, or provide `--owner-id`, `--organization-id`, and `--repository-id`.\"\n                    (getCorrelationId parseResult)\n            )\n\n    let private ensureRepositoryContextIsComplete (graceIds: GraceIds) (parseResult: ParseResult) =\n        if\n            graceIds.OwnerId = Guid.Empty\n            && String.IsNullOrWhiteSpace(graceIds.OwnerName)\n        then\n            Error(GraceError.Create (getErrorMessage OwnerError.EitherOwnerIdOrOwnerNameRequired) (getCorrelationId parseResult))\n        elif\n            graceIds.OrganizationId = Guid.Empty\n            && String.IsNullOrWhiteSpace(graceIds.OrganizationName)\n        then\n            Error(GraceError.Create (getErrorMessage OrganizationError.EitherOrganizationIdOrOrganizationNameRequired) (getCorrelationId parseResult))\n        elif\n            graceIds.RepositoryId = Guid.Empty\n            && String.IsNullOrWhiteSpace(graceIds.RepositoryName)\n        then\n            Error(GraceError.Create (getErrorMessage RepositoryError.EitherRepositoryIdOrRepositoryNameRequired) (getCorrelationId parseResult))\n        else\n            Ok()\n\n    let private getLocalStateFilePath () =\n        let graceDirectory =\n            if configurationFileExists () then\n                Current().GraceDirectory\n            else\n                Path.Combine(Environment.CurrentDirectory, Constants.GraceConfigDirectory)\n\n        Path.GetFullPath(Path.Combine(graceDirectory, localStateFileName))\n\n    let private tryReadLocalState (parseResult: ParseResult) =\n        let stateFilePath = getLocalStateFilePath ()\n        let correlationId = getCorrelationId parseResult\n\n        try\n            if not <| File.Exists(stateFilePath) then\n                Ok(stateFilePath, localAgentSessionStateDefault)\n            else\n                let fileContent = File.ReadAllText(stateFilePath)\n\n                if String.IsNullOrWhiteSpace(fileContent) then\n                    Ok(stateFilePath, localAgentSessionStateDefault)\n                else\n                    let state = deserialize<LocalAgentSessionState> fileContent\n                    let normalizedState = if isNull (box state) then localAgentSessionStateDefault else state\n                    Ok(stateFilePath, normalizedState)\n        with\n        | ex ->\n            Error(\n                GraceError.Create\n                    ($\"Unable to read local agent session state from {stateFilePath}. Run `grace agent bootstrap --agent-id <Guid> --display-name <Name>` to reset local state. Details: {ex.Message}\")\n                    correlationId\n            )\n\n    let private tryWriteLocalState (parseResult: ParseResult) (stateFilePath: string) (state: LocalAgentSessionState) =\n        let correlationId = getCorrelationId parseResult\n\n        try\n            Directory.CreateDirectory(Path.GetDirectoryName(stateFilePath))\n            |> ignore\n\n            File.WriteAllText(stateFilePath, serialize state)\n            Ok()\n        with\n        | ex -> Error(GraceError.Create ($\"Unable to write local agent session state to {stateFilePath}: {ex.Message}\") correlationId)\n\n    let private hasBootstrappedIdentity (state: LocalAgentSessionState) =\n        not <| String.IsNullOrWhiteSpace(state.AgentId)\n        && not\n           <| String.IsNullOrWhiteSpace(state.AgentDisplayName)\n\n    let private hasActiveSession (state: LocalAgentSessionState) =\n        not\n        <| String.IsNullOrWhiteSpace(state.ActiveSessionId)\n        || not\n           <| String.IsNullOrWhiteSpace(state.ActiveWorkItemIdOrNumber)\n\n    let private createDefaultOperationId (prefix: string) (correlationId: string) = $\"{prefix}:{correlationId}\"\n\n    let private normalizeOperationId (explicitOperationId: string option) (fallbackPrefix: string) (correlationId: string) =\n        explicitOperationId\n        |> Option.defaultValue (createDefaultOperationId fallbackPrefix correlationId)\n\n    let private staleStateError (parseResult: ParseResult) (details: string) =\n        GraceError.Create $\"{details} Run `grace agent work status` and then `grace agent work stop` to reconcile local state.\" (getCorrelationId parseResult)\n\n    let private ensureBootstrapped (parseResult: ParseResult) (state: LocalAgentSessionState) =\n        if hasBootstrappedIdentity state then\n            Ok()\n        else\n            Error(\n                GraceError.Create\n                    \"Agent identity is not bootstrapped. Run `grace agent bootstrap --agent-id <Guid> --display-name <Name>` first.\"\n                    (getCorrelationId parseResult)\n            )\n\n    let private toSessionInfo (state: LocalAgentSessionState) (lifecycleState: AgentSessionLifecycleState) =\n        { AgentSessionInfo.Default with\n            SessionId = state.ActiveSessionId\n            AgentId = state.AgentId\n            AgentDisplayName = state.AgentDisplayName\n            WorkItemIdOrNumber = state.ActiveWorkItemIdOrNumber\n            PromotionSetId = state.ActivePromotionSetId\n            Source = state.Source\n            LifecycleState = lifecycleState\n        }\n\n    let private createLocalOperationResult\n        (state: LocalAgentSessionState)\n        (lifecycleState: AgentSessionLifecycleState)\n        (message: string)\n        (operationId: string)\n        (wasReplay: bool)\n        =\n        { AgentSessionOperationResult.Default with\n            Session = toSessionInfo state lifecycleState\n            Message = message\n            OperationId = operationId\n            WasIdempotentReplay = wasReplay\n        }\n\n    let private writeOperationSummary (parseResult: ParseResult) (result: AgentSessionOperationResult) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let lifecycleState = getDiscriminatedUnionCaseName result.Session.LifecycleState\n            AnsiConsole.MarkupLine($\"[green]{Markup.Escape(result.Message)}[/]\")\n\n            if\n                not\n                <| String.IsNullOrWhiteSpace(result.Session.SessionId)\n            then\n                AnsiConsole.MarkupLine($\"[bold]Session:[/] {Markup.Escape(result.Session.SessionId)}\")\n\n            if\n                not\n                <| String.IsNullOrWhiteSpace(result.Session.WorkItemIdOrNumber)\n            then\n                AnsiConsole.MarkupLine($\"[bold]Work Item:[/] {Markup.Escape(result.Session.WorkItemIdOrNumber)}\")\n\n            if\n                not\n                <| String.IsNullOrWhiteSpace(result.OperationId)\n            then\n                AnsiConsole.MarkupLine($\"[bold]Operation:[/] {Markup.Escape(result.OperationId)}\")\n\n            AnsiConsole.MarkupLine($\"[bold]State:[/] {Markup.Escape(lifecycleState)}\")\n\n            if result.WasIdempotentReplay then\n                AnsiConsole.MarkupLine(\"[yellow]Operation was handled as an idempotent replay.[/]\")\n\n    let private clearActiveSession (state: LocalAgentSessionState) (operationId: string) (correlationId: string) =\n        { state with\n            ActiveSessionId = String.Empty\n            ActiveWorkItemIdOrNumber = String.Empty\n            ActivePromotionSetId = String.Empty\n            LastOperationId = operationId\n            LastCorrelationId = correlationId\n            LastUpdatedAtUtc = DateTime.UtcNow\n        }\n\n    let private tryNormalizePromotionSetId (value: string option) (parseResult: ParseResult) =\n        match value with\n        | None -> Ok String.Empty\n        | Some promotionSetId ->\n            match tryParseGuid promotionSetId \"Promotion set ID must be a valid non-empty Guid.\" parseResult with\n            | Error error -> Error error\n            | Ok parsed -> Ok(parsed.ToString())\n\n    let private inferMimeType (filePath: string) =\n        match Path.GetExtension(filePath).ToLowerInvariant() with\n        | \".md\" -> \"text/markdown\"\n        | \".txt\" -> \"text/plain\"\n        | \".json\" -> \"application/json\"\n        | _ -> \"application/octet-stream\"\n\n    let private tryReadFileContent (filePath: string) (displayName: string) (parseResult: ParseResult) =\n        try\n            Ok(File.ReadAllText(filePath))\n        with\n        | ex -> Error(GraceError.Create ($\"Failed to read {displayName} file '{filePath}': {ex.Message}\") (getCorrelationId parseResult))\n\n    let private addSummaryHandler (parseResult: ParseResult) =\n        async {\n            if verbose parseResult then printParseResult parseResult\n\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n            let workItemIdRaw = parseResult.GetValue(Options.addSummaryWorkItemId)\n            let summaryFilePath = parseResult.GetValue(Options.summaryFile)\n\n            let promptOrigin =\n                tryGetOptionString parseResult Options.promptOrigin\n                |> Option.defaultValue String.Empty\n\n            let promotionSetIdOption = tryGetOptionString parseResult Options.addSummaryPromotionSetId\n\n            let promptFilePath =\n                parseResult.GetValue(Options.promptFile)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            match ensureRepositoryContextIsComplete graceIds parseResult with\n            | Error error -> return Error error\n            | Ok _ ->\n                match tryNormalizeWorkItemIdentifier workItemIdRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    if not <| File.Exists(summaryFilePath) then\n                        return Error(GraceError.Create $\"Summary file does not exist: {summaryFilePath}\" (getCorrelationId parseResult))\n                    elif\n                        not (String.IsNullOrWhiteSpace(promptFilePath))\n                        && not <| File.Exists(promptFilePath)\n                    then\n                        return Error(GraceError.Create $\"Prompt file does not exist: {promptFilePath}\" (getCorrelationId parseResult))\n                    elif\n                        not (String.IsNullOrWhiteSpace(promptOrigin))\n                        && String.IsNullOrWhiteSpace(promptFilePath)\n                    then\n                        return Error(GraceError.Create \"Prompt origin can only be provided when --prompt-file is provided.\" (getCorrelationId parseResult))\n                    else\n                        match tryNormalizePromotionSetId promotionSetIdOption parseResult with\n                        | Error error -> return Error error\n                        | Ok promotionSetId ->\n                            match tryReadFileContent summaryFilePath \"summary\" parseResult with\n                            | Error error -> return Error error\n                            | Ok summaryContent ->\n                                let promptContentResult =\n                                    if String.IsNullOrWhiteSpace(promptFilePath) then\n                                        Ok String.Empty\n                                    else\n                                        tryReadFileContent promptFilePath \"prompt\" parseResult\n\n                                match promptContentResult with\n                                | Error error -> return Error error\n                                | Ok promptContent ->\n                                    let parameters =\n                                        Parameters.WorkItem.AddSummaryParameters(\n                                            WorkItemId = workItem,\n                                            SummaryContent = summaryContent,\n                                            SummaryMimeType = inferMimeType summaryFilePath,\n                                            PromptContent = promptContent,\n                                            PromptMimeType =\n                                                (if String.IsNullOrWhiteSpace(promptFilePath) then\n                                                     String.Empty\n                                                 else\n                                                     inferMimeType promptFilePath),\n                                            PromptOrigin = promptOrigin,\n                                            PromotionSetId = promotionSetId,\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OwnerName = graceIds.OwnerName,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            OrganizationName = graceIds.OrganizationName,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            RepositoryName = graceIds.RepositoryName,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    let! addSummaryResponse = WorkItem.AddSummary(parameters) |> Async.AwaitTask\n\n                                    match addSummaryResponse with\n                                    | Error error -> return Error error\n                                    | Ok addSummaryResult ->\n                                        let response = addSummaryResult.ReturnValue\n\n                                        let promptArtifactId =\n                                            response.PromptArtifactId\n                                            |> Option.ofObj\n                                            |> Option.map (fun value -> value.Trim())\n                                            |> Option.filter (fun value -> not <| String.IsNullOrWhiteSpace(value))\n\n                                        let promotionSetIdResult =\n                                            response.PromotionSetId\n                                            |> Option.ofObj\n                                            |> Option.map (fun value -> value.Trim())\n                                            |> Option.filter (fun value -> not <| String.IsNullOrWhiteSpace(value))\n\n                                        let summaryArtifactId =\n                                            response.SummaryArtifactId\n                                            |> Option.ofObj\n                                            |> Option.map (fun value -> value.Trim())\n                                            |> Option.filter (fun value -> not <| String.IsNullOrWhiteSpace(value))\n                                            |> Option.defaultValue String.Empty\n\n                                        let result =\n                                            {\n                                                WorkItem = workItem\n                                                SummaryArtifactId = summaryArtifactId\n                                                PromptArtifactId = promptArtifactId\n                                                PromotionSetId = promotionSetIdResult\n                                            }\n\n                                        if\n                                            not (parseResult |> json)\n                                            && not (parseResult |> silent)\n                                        then\n                                            AnsiConsole.MarkupLine(\n                                                $\"[green]Linked AgentSummary artifact[/] {Markup.Escape(result.SummaryArtifactId)} [green]to work item[/] {Markup.Escape(workItem)}\"\n                                            )\n\n                                            match result.PromptArtifactId with\n                                            | Some artifactId ->\n                                                AnsiConsole.MarkupLine(\n                                                    $\"[green]Linked Prompt artifact[/] {Markup.Escape(artifactId)} [green]to work item[/] {Markup.Escape(workItem)}\"\n                                                )\n                                            | None -> ()\n\n                                            match result.PromotionSetId with\n                                            | Some linkedPromotionSetId ->\n                                                AnsiConsole.MarkupLine(\n                                                    $\"[green]Linked promotion set[/] {Markup.Escape(linkedPromotionSetId)} [green]to work item[/] {Markup.Escape(workItem)}\"\n                                                )\n                                            | None -> ()\n\n                                        return Ok(GraceReturnValue.Create result graceIds.CorrelationId)\n        }\n        |> Async.StartAsTask\n\n    let private bootstrapHandler (parseResult: ParseResult) =\n        async {\n            let correlationId = getCorrelationId parseResult\n            let stateFilePath = getLocalStateFilePath ()\n\n            let existingState =\n                match tryReadLocalState parseResult with\n                | Ok (_, state) -> state\n                | Error _ -> localAgentSessionStateDefault\n\n            let agentIdRaw =\n                parseResult.GetValue(Options.agentId)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            let displayName =\n                tryGetOptionString parseResult Options.displayName\n                |> Option.defaultValue String.Empty\n\n            let source =\n                tryGetOptionString parseResult Options.source\n                |> Option.defaultValue \"cli\"\n\n            match tryParseGuid agentIdRaw \"Agent ID must be a valid non-empty Guid.\" parseResult with\n            | Error error -> return Error error\n            | Ok parsedAgentId ->\n                if String.IsNullOrWhiteSpace(displayName) then\n                    return Error(GraceError.Create \"Display name is required.\" correlationId)\n                elif\n                    hasActiveSession existingState\n                    && hasBootstrappedIdentity existingState\n                    && not (existingState.AgentId.Equals(parsedAgentId.ToString(), StringComparison.OrdinalIgnoreCase))\n                then\n                    return\n                        Error(\n                            staleStateError\n                                parseResult\n                                ($\"Local state contains an active session for agent '{existingState.AgentId}'. Stop that session before bootstrapping a different agent.\")\n                        )\n                else\n                    let normalizedSource = if String.IsNullOrWhiteSpace(source) then \"cli\" else source\n\n                    let unchanged =\n                        existingState.AgentId.Equals(parsedAgentId.ToString(), StringComparison.OrdinalIgnoreCase)\n                        && existingState.AgentDisplayName.Equals(displayName, StringComparison.Ordinal)\n                        && existingState.Source.Equals(normalizedSource, StringComparison.Ordinal)\n\n                    let operationId = createDefaultOperationId \"bootstrap\" correlationId\n\n                    let updatedState =\n                        { existingState with\n                            AgentId = parsedAgentId.ToString()\n                            AgentDisplayName = displayName\n                            Source = normalizedSource\n                            LastOperationId = operationId\n                            LastCorrelationId = correlationId\n                            LastUpdatedAtUtc = DateTime.UtcNow\n                        }\n\n                    match tryWriteLocalState parseResult stateFilePath updatedState with\n                    | Error error -> return Error error\n                    | Ok _ ->\n                        let lifecycleState =\n                            if hasActiveSession updatedState then\n                                AgentSessionLifecycleState.Active\n                            else\n                                AgentSessionLifecycleState.Inactive\n\n                        let message =\n                            if unchanged then\n                                \"Agent identity already bootstrapped. Reusing existing local state.\"\n                            else\n                                \"Agent identity bootstrapped successfully.\"\n\n                        let operationResult = createLocalOperationResult updatedState lifecycleState message operationId unchanged\n                        writeOperationSummary parseResult operationResult\n                        return Ok(GraceReturnValue.Create operationResult correlationId)\n        }\n        |> Async.StartAsTask\n\n    let internal workStartWith (startSession: StartAgentSessionParameters -> Task<GraceResult<AgentSessionOperationResult>>) (parseResult: ParseResult) =\n        async {\n            match ensureRepositoryContextIsAvailable parseResult with\n            | Error error -> return Error error\n            | Ok _ ->\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match ensureRepositoryContextIsComplete graceIds parseResult with\n                | Error error -> return Error error\n                | Ok _ ->\n                    let workItemIdRaw = parseResult.GetValue(Options.startWorkItemId)\n\n                    match tryNormalizeWorkItemIdentifier workItemIdRaw parseResult with\n                    | Error error -> return Error error\n                    | Ok workItemId ->\n                        let promotionSetIdOption = tryGetOptionString parseResult Options.promotionSetId\n\n                        match tryNormalizePromotionSetId promotionSetIdOption parseResult with\n                        | Error error -> return Error error\n                        | Ok promotionSetId ->\n                            match tryReadLocalState parseResult with\n                            | Error error -> return Error error\n                            | Ok (stateFilePath, state) ->\n                                match ensureBootstrapped parseResult state with\n                                | Error error -> return Error error\n                                | Ok _ ->\n                                    if hasActiveSession state then\n                                        if state.ActiveWorkItemIdOrNumber = workItemId then\n                                            let operationId =\n                                                if String.IsNullOrWhiteSpace(state.LastOperationId) then\n                                                    createDefaultOperationId \"start\" graceIds.CorrelationId\n                                                else\n                                                    state.LastOperationId\n\n                                            let operationResult =\n                                                createLocalOperationResult\n                                                    state\n                                                    AgentSessionLifecycleState.Active\n                                                    \"Work session is already active for this work item.\"\n                                                    operationId\n                                                    true\n\n                                            writeOperationSummary parseResult operationResult\n                                            return Ok(GraceReturnValue.Create operationResult graceIds.CorrelationId)\n                                        else\n                                            return\n                                                Error(\n                                                    staleStateError\n                                                        parseResult\n                                                        ($\"Local state indicates an active session for work item '{state.ActiveWorkItemIdOrNumber}', but start requested '{workItemId}'.\")\n                                                )\n                                    else\n                                        let operationId =\n                                            normalizeOperationId (tryGetOptionString parseResult Options.operationId) \"start\" graceIds.CorrelationId\n\n                                        let sourceFromState = if String.IsNullOrWhiteSpace(state.Source) then \"cli\" else state.Source\n\n                                        let source =\n                                            tryGetOptionString parseResult Options.source\n                                            |> Option.defaultValue sourceFromState\n\n                                        let startParameters =\n                                            StartAgentSessionParameters(\n                                                WorkItemIdOrNumber = workItemId,\n                                                PromotionSetId = promotionSetId,\n                                                Source = source,\n                                                OperationId = operationId,\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                AgentId = state.AgentId,\n                                                AgentDisplayName = state.AgentDisplayName,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        let! startResult = startSession startParameters |> Async.AwaitTask\n\n                                        match startResult with\n                                        | Error error -> return Error error\n                                        | Ok returnValue ->\n                                            let normalizedResult =\n                                                if String.IsNullOrWhiteSpace(returnValue.ReturnValue.OperationId) then\n                                                    { returnValue.ReturnValue with OperationId = operationId }\n                                                else\n                                                    returnValue.ReturnValue\n\n                                            let session = normalizedResult.Session\n\n                                            let updatedState =\n                                                { state with\n                                                    AgentId =\n                                                        if String.IsNullOrWhiteSpace(session.AgentId) then\n                                                            state.AgentId\n                                                        else\n                                                            session.AgentId\n                                                    AgentDisplayName =\n                                                        if String.IsNullOrWhiteSpace(session.AgentDisplayName) then\n                                                            state.AgentDisplayName\n                                                        else\n                                                            session.AgentDisplayName\n                                                    Source = if String.IsNullOrWhiteSpace(session.Source) then source else session.Source\n                                                    ActiveSessionId =\n                                                        if String.IsNullOrWhiteSpace(session.SessionId) then\n                                                            operationId\n                                                        else\n                                                            session.SessionId\n                                                    ActiveWorkItemIdOrNumber =\n                                                        if String.IsNullOrWhiteSpace(session.WorkItemIdOrNumber) then\n                                                            workItemId\n                                                        else\n                                                            session.WorkItemIdOrNumber\n                                                    ActivePromotionSetId =\n                                                        if String.IsNullOrWhiteSpace(session.PromotionSetId) then\n                                                            promotionSetId\n                                                        else\n                                                            session.PromotionSetId\n                                                    LastOperationId = normalizedResult.OperationId\n                                                    LastCorrelationId = graceIds.CorrelationId\n                                                    LastUpdatedAtUtc = DateTime.UtcNow\n                                                }\n\n                                            match tryWriteLocalState parseResult stateFilePath updatedState with\n                                            | Error error -> return Error error\n                                            | Ok _ ->\n                                                writeOperationSummary parseResult normalizedResult\n                                                return Ok({ returnValue with ReturnValue = normalizedResult })\n        }\n        |> Async.StartAsTask\n\n    let internal workStopWith (stopSession: StopAgentSessionParameters -> Task<GraceResult<AgentSessionOperationResult>>) (parseResult: ParseResult) =\n        async {\n            match ensureRepositoryContextIsAvailable parseResult with\n            | Error error -> return Error error\n            | Ok _ ->\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match ensureRepositoryContextIsComplete graceIds parseResult with\n                | Error error -> return Error error\n                | Ok _ ->\n                    match tryReadLocalState parseResult with\n                    | Error error -> return Error error\n                    | Ok (stateFilePath, state) ->\n                        match ensureBootstrapped parseResult state with\n                        | Error error -> return Error error\n                        | Ok _ ->\n                            let requestedSessionId =\n                                tryGetOptionString parseResult Options.sessionId\n                                |> Option.defaultValue String.Empty\n\n                            let requestedWorkItemRaw = tryGetOptionString parseResult Options.optionalWorkItemId\n\n                            let normalizedRequestedWorkItemResult =\n                                match requestedWorkItemRaw with\n                                | None -> Ok String.Empty\n                                | Some workItemRaw -> tryNormalizeWorkItemIdentifier workItemRaw parseResult\n\n                            match normalizedRequestedWorkItemResult with\n                            | Error error -> return Error error\n                            | Ok requestedWorkItemId ->\n                                if\n                                    hasActiveSession state\n                                    && not\n                                       <| String.IsNullOrWhiteSpace(requestedSessionId)\n                                    && not (state.ActiveSessionId.Equals(requestedSessionId, StringComparison.OrdinalIgnoreCase))\n                                then\n                                    return\n                                        Error(\n                                            staleStateError\n                                                parseResult\n                                                ($\"Local state indicates session '{state.ActiveSessionId}', but stop requested session '{requestedSessionId}'.\")\n                                        )\n                                elif\n                                    hasActiveSession state\n                                    && not\n                                       <| String.IsNullOrWhiteSpace(requestedWorkItemId)\n                                    && not (state.ActiveWorkItemIdOrNumber.Equals(requestedWorkItemId, StringComparison.OrdinalIgnoreCase))\n                                then\n                                    return\n                                        Error(\n                                            staleStateError\n                                                parseResult\n                                                ($\"Local state indicates work item '{state.ActiveWorkItemIdOrNumber}', but stop requested work item '{requestedWorkItemId}'.\")\n                                        )\n                                elif\n                                    not (hasActiveSession state)\n                                    && String.IsNullOrWhiteSpace(requestedSessionId)\n                                    && String.IsNullOrWhiteSpace(requestedWorkItemId)\n                                then\n                                    let operationId = createDefaultOperationId \"stop\" graceIds.CorrelationId\n                                    let updatedState = clearActiveSession state operationId graceIds.CorrelationId\n\n                                    match tryWriteLocalState parseResult stateFilePath updatedState with\n                                    | Error error -> return Error error\n                                    | Ok _ ->\n                                        let operationResult =\n                                            createLocalOperationResult\n                                                updatedState\n                                                AgentSessionLifecycleState.Inactive\n                                                \"No active local work session was found. Nothing to stop.\"\n                                                operationId\n                                                true\n\n                                        writeOperationSummary parseResult operationResult\n                                        return Ok(GraceReturnValue.Create operationResult graceIds.CorrelationId)\n                                else\n                                    let operationId = normalizeOperationId (tryGetOptionString parseResult Options.operationId) \"stop\" graceIds.CorrelationId\n\n                                    let sessionId =\n                                        if String.IsNullOrWhiteSpace(requestedSessionId) then\n                                            state.ActiveSessionId\n                                        else\n                                            requestedSessionId\n\n                                    let workItemId =\n                                        if String.IsNullOrWhiteSpace(requestedWorkItemId) then\n                                            state.ActiveWorkItemIdOrNumber\n                                        else\n                                            requestedWorkItemId\n\n                                    let stopParameters =\n                                        StopAgentSessionParameters(\n                                            SessionId = sessionId,\n                                            WorkItemIdOrNumber = workItemId,\n                                            StopReason =\n                                                (tryGetOptionString parseResult Options.stopReason\n                                                 |> Option.defaultValue String.Empty),\n                                            OperationId = operationId,\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OwnerName = graceIds.OwnerName,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            OrganizationName = graceIds.OrganizationName,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            RepositoryName = graceIds.RepositoryName,\n                                            AgentId = state.AgentId,\n                                            AgentDisplayName = state.AgentDisplayName,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    let! stopResult = stopSession stopParameters |> Async.AwaitTask\n\n                                    match stopResult with\n                                    | Error error -> return Error error\n                                    | Ok returnValue ->\n                                        let normalizedResult =\n                                            if String.IsNullOrWhiteSpace(returnValue.ReturnValue.OperationId) then\n                                                { returnValue.ReturnValue with OperationId = operationId }\n                                            else\n                                                returnValue.ReturnValue\n\n                                        let clearedState = clearActiveSession state normalizedResult.OperationId graceIds.CorrelationId\n\n                                        match tryWriteLocalState parseResult stateFilePath clearedState with\n                                        | Error error -> return Error error\n                                        | Ok _ ->\n                                            writeOperationSummary parseResult normalizedResult\n                                            return Ok({ returnValue with ReturnValue = normalizedResult })\n        }\n        |> Async.StartAsTask\n\n    let internal workStatusWith (getStatus: GetAgentSessionStatusParameters -> Task<GraceResult<AgentSessionOperationResult>>) (parseResult: ParseResult) =\n        async {\n            match ensureRepositoryContextIsAvailable parseResult with\n            | Error error -> return Error error\n            | Ok _ ->\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match ensureRepositoryContextIsComplete graceIds parseResult with\n                | Error error -> return Error error\n                | Ok _ ->\n                    match tryReadLocalState parseResult with\n                    | Error error -> return Error error\n                    | Ok (stateFilePath, state) ->\n                        match ensureBootstrapped parseResult state with\n                        | Error error -> return Error error\n                        | Ok _ ->\n                            let requestedSessionId =\n                                tryGetOptionString parseResult Options.sessionId\n                                |> Option.defaultValue String.Empty\n\n                            let requestedWorkItemRaw = tryGetOptionString parseResult Options.optionalWorkItemId\n\n                            let normalizedRequestedWorkItemResult =\n                                match requestedWorkItemRaw with\n                                | None -> Ok String.Empty\n                                | Some workItemRaw -> tryNormalizeWorkItemIdentifier workItemRaw parseResult\n\n                            match normalizedRequestedWorkItemResult with\n                            | Error error -> return Error error\n                            | Ok requestedWorkItemId ->\n                                if\n                                    hasActiveSession state\n                                    && not\n                                       <| String.IsNullOrWhiteSpace(requestedSessionId)\n                                    && not (state.ActiveSessionId.Equals(requestedSessionId, StringComparison.OrdinalIgnoreCase))\n                                then\n                                    return\n                                        Error(\n                                            staleStateError\n                                                parseResult\n                                                ($\"Local state indicates session '{state.ActiveSessionId}', but status requested session '{requestedSessionId}'.\")\n                                        )\n                                elif\n                                    hasActiveSession state\n                                    && not\n                                       <| String.IsNullOrWhiteSpace(requestedWorkItemId)\n                                    && not (state.ActiveWorkItemIdOrNumber.Equals(requestedWorkItemId, StringComparison.OrdinalIgnoreCase))\n                                then\n                                    return\n                                        Error(\n                                            staleStateError\n                                                parseResult\n                                                ($\"Local state indicates work item '{state.ActiveWorkItemIdOrNumber}', but status requested work item '{requestedWorkItemId}'.\")\n                                        )\n                                else\n                                    let sessionId =\n                                        if String.IsNullOrWhiteSpace(requestedSessionId) then\n                                            state.ActiveSessionId\n                                        else\n                                            requestedSessionId\n\n                                    let workItemId =\n                                        if String.IsNullOrWhiteSpace(requestedWorkItemId) then\n                                            state.ActiveWorkItemIdOrNumber\n                                        else\n                                            requestedWorkItemId\n\n                                    if\n                                        String.IsNullOrWhiteSpace(sessionId)\n                                        && String.IsNullOrWhiteSpace(workItemId)\n                                    then\n                                        return\n                                            Error(\n                                                GraceError.Create\n                                                    \"No active local work session is available. Run `grace agent work start --work-item-id <id>` first, or provide `--session-id` or `--work-item-id`.\"\n                                                    (getCorrelationId parseResult)\n                                            )\n                                    else\n                                        let statusParameters =\n                                            GetAgentSessionStatusParameters(\n                                                SessionId = sessionId,\n                                                WorkItemIdOrNumber = workItemId,\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                AgentId = state.AgentId,\n                                                AgentDisplayName = state.AgentDisplayName,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        let! statusResult = getStatus statusParameters |> Async.AwaitTask\n\n                                        match statusResult with\n                                        | Error error -> return Error error\n                                        | Ok returnValue ->\n                                            let session = returnValue.ReturnValue.Session\n\n                                            let updatedState =\n                                                match session.LifecycleState with\n                                                | AgentSessionLifecycleState.Active\n                                                | AgentSessionLifecycleState.Stopping ->\n                                                    { state with\n                                                        ActiveSessionId = session.SessionId\n                                                        ActiveWorkItemIdOrNumber = session.WorkItemIdOrNumber\n                                                        ActivePromotionSetId = session.PromotionSetId\n                                                        LastOperationId = returnValue.ReturnValue.OperationId\n                                                        LastCorrelationId = graceIds.CorrelationId\n                                                        LastUpdatedAtUtc = DateTime.UtcNow\n                                                    }\n                                                | AgentSessionLifecycleState.Inactive\n                                                | AgentSessionLifecycleState.Stopped ->\n                                                    clearActiveSession state returnValue.ReturnValue.OperationId graceIds.CorrelationId\n\n                                            match tryWriteLocalState parseResult stateFilePath updatedState with\n                                            | Error error -> return Error error\n                                            | Ok _ ->\n                                                writeOperationSummary parseResult returnValue.ReturnValue\n                                                return Ok returnValue\n        }\n        |> Async.StartAsTask\n\n    let private workStartHandler (parseResult: ParseResult) = workStartWith AgentSession.Start parseResult\n    let private workStopHandler (parseResult: ParseResult) = workStopWith AgentSession.Stop parseResult\n    let private workStatusHandler (parseResult: ParseResult) = workStatusWith AgentSession.Status parseResult\n\n    type AddSummary() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = addSummaryHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type Bootstrap() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = bootstrapHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type WorkStart() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = workStartHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type WorkStop() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = workStopHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type WorkStatus() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = workStatusHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let agentCommand = new Command(\"agent\", Description = \"Agent workflow commands.\")\n\n        let bootstrapCommand =\n            new Command(\"bootstrap\", Description = \"Bootstrap local agent identity for deterministic session workflows.\")\n            |> addOption Options.agentId\n            |> addOption Options.displayName\n            |> addOption Options.source\n\n        bootstrapCommand.Action <- new Bootstrap()\n\n        let workCommand = new Command(\"work\", Description = \"Manage agent work sessions.\")\n\n        let workStartCommand =\n            new Command(\"start\", Description = \"Start work on a work item.\")\n            |> addOption Options.startWorkItemId\n            |> addOption Options.promotionSetId\n            |> addOption Options.source\n            |> addOption Options.operationId\n            |> addCommonOptions\n\n        workStartCommand.Action <- new WorkStart()\n\n        let workStopCommand =\n            new Command(\"stop\", Description = \"Stop the current or specified work session.\")\n            |> addOption Options.sessionId\n            |> addOption Options.optionalWorkItemId\n            |> addOption Options.stopReason\n            |> addOption Options.operationId\n            |> addCommonOptions\n\n        workStopCommand.Action <- new WorkStop()\n\n        let workStatusCommand =\n            new Command(\"status\", Description = \"Get status for the current or specified work session.\")\n            |> addOption Options.sessionId\n            |> addOption Options.optionalWorkItemId\n            |> addCommonOptions\n\n        workStatusCommand.Action <- new WorkStatus()\n\n        workCommand.Subcommands.Add(workStartCommand)\n        workCommand.Subcommands.Add(workStopCommand)\n        workCommand.Subcommands.Add(workStatusCommand)\n\n        let addSummaryCommand =\n            new Command(\"add-summary\", Description = \"Submit summary content (and optional prompt content) and link canonical artifacts to a work item.\")\n            |> addOption Options.addSummaryWorkItemId\n            |> addOption Options.summaryFile\n            |> addOption Options.promptFile\n            |> addOption Options.promptOrigin\n            |> addOption Options.addSummaryPromotionSetId\n            |> addCommonOptions\n\n        addSummaryCommand.Action <- new AddSummary()\n        agentCommand.Subcommands.Add(bootstrapCommand)\n        agentCommand.Subcommands.Add(workCommand)\n        agentCommand.Subcommands.Add(addSummaryCommand)\n        agentCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Auth.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Microsoft.Identity.Client.Extensions.Msal\nopen Spectre.Console\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Net\nopen System.Net.Http\nopen System.Security.Cryptography\nopen System.Text\nopen System.Text.Json\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.IO\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule Auth =\n\n    type LoginMode =\n        | Pkce\n        | Device\n\n    type OidcCliConfig = { Authority: string; Audience: string; ClientId: string; RedirectPort: int; Scopes: string list }\n\n    type OidcM2mConfig = { Authority: string; Audience: string; ClientId: string; ClientSecret: string; Scopes: string list }\n\n    type AuthInfo = { GraceUserId: string; Claims: string list }\n\n    type TokenBundle =\n        {\n            RefreshToken: string\n            AccessToken: string\n            AccessTokenExpiresAt: Instant\n            Issuer: string\n            Audience: string\n            Scopes: string\n            Subject: string option\n            ClientId: string\n            CreatedAt: Instant\n            UpdatedAt: Instant\n        }\n\n    type TokenResponse = { AccessToken: string; RefreshToken: string option; ExpiresIn: int option; Scope: string option; TokenType: string option }\n\n    type DeviceCodeResponse =\n        {\n            DeviceCode: string\n            UserCode: string\n            VerificationUri: string\n            VerificationUriComplete: string option\n            ExpiresIn: int\n            IntervalSeconds: int\n        }\n\n    type TokenStore = { Helper: MsalCacheHelper; StorageProperties: StorageCreationProperties; LockFilePath: string; InProcessLock: SemaphoreSlim }\n\n    let private tryGetEnv name =\n        let value = Environment.GetEnvironmentVariable(name)\n        if String.IsNullOrWhiteSpace value then None else Some value\n\n    let private normalizeBearerToken (token: string) =\n        if token.StartsWith(\"Bearer \", StringComparison.OrdinalIgnoreCase) then\n            token.Substring(\"Bearer \".Length).Trim()\n        else\n            token.Trim()\n\n    let private normalizeAuthority (authority: string) =\n        let trimmed = authority.Trim()\n\n        if trimmed.EndsWith(\"/\", StringComparison.Ordinal) then\n            trimmed\n        else\n            $\"{trimmed}/\"\n\n    let private parseScopes (value: string) =\n        value.Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries)\n        |> Seq.map (fun scopeValue -> scopeValue.Trim())\n        |> Seq.filter (fun scopeValue -> not (String.IsNullOrWhiteSpace scopeValue))\n        |> Seq.toList\n\n    let private defaultCliScopes () =\n        [\n            \"openid\"\n            \"profile\"\n            \"email\"\n            \"offline_access\"\n        ]\n\n    let private buildOidcCliConfig (authority: string) (audience: string) (clientId: string) =\n        let redirectPort =\n            match tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcCliRedirectPort with\n            | Some raw ->\n                match Int32.TryParse raw with\n                | true, parsed when parsed > 0 -> parsed\n                | _ -> 8391\n            | None -> 8391\n\n        let scopes =\n            match tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcCliScopes with\n            | Some raw when not (String.IsNullOrWhiteSpace raw) -> parseScopes raw\n            | _ -> defaultCliScopes ()\n\n        { Authority = normalizeAuthority authority; Audience = audience.Trim(); ClientId = clientId.Trim(); RedirectPort = redirectPort; Scopes = scopes }\n\n    let private tryGetOidcCliConfigFromEnv () =\n        match tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcAuthority,\n              tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcAudience,\n              tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcCliClientId\n            with\n        | Some authority, Some audience, Some clientId -> Some(buildOidcCliConfig authority audience clientId)\n        | _ -> None\n\n    let private tryGetOidcCliConfigFromServer (correlationId: string) =\n        task {\n            match tryGetEnv Constants.EnvironmentVariables.GraceServerUri with\n            | None -> return Ok None\n            | Some _ ->\n                let parameters = CommonParameters(CorrelationId = correlationId)\n                let! result = Grace.SDK.Auth.getOidcClientConfig parameters\n\n                match result with\n                | Ok graceReturnValue ->\n                    let config = graceReturnValue.ReturnValue\n                    return Ok(Some(buildOidcCliConfig config.Authority config.Audience config.CliClientId))\n                | Error error -> return Error error\n        }\n\n    let private tryGetOidcCliConfig (correlationId: string) =\n        task {\n            match tryGetOidcCliConfigFromEnv () with\n            | Some config -> return Ok(Some config)\n            | None -> return! tryGetOidcCliConfigFromServer correlationId\n        }\n\n    let private tryGetOidcM2mConfig () =\n        match tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcAuthority,\n              tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcAudience,\n              tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientId,\n              tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret\n            with\n        | Some authority, Some audience, Some clientId, Some clientSecret ->\n            let scopes =\n                match tryGetEnv Constants.EnvironmentVariables.GraceAuthOidcM2mScopes with\n                | Some raw when not (String.IsNullOrWhiteSpace raw) -> parseScopes raw\n                | _ -> []\n\n            Some\n                {\n                    Authority = normalizeAuthority authority\n                    Audience = audience.Trim()\n                    ClientId = clientId.Trim()\n                    ClientSecret = clientSecret\n                    Scopes = scopes\n                }\n        | _ -> None\n\n    let private tryGetGraceTokenFromEnv () =\n        match tryGetEnv Constants.EnvironmentVariables.GraceToken with\n        | None -> Ok None\n        | Some value ->\n            let normalized = normalizeBearerToken value\n\n            if String.IsNullOrWhiteSpace normalized then\n                Error $\"GRACE_TOKEN is set but empty. Provide a Grace PAT or unset {Constants.EnvironmentVariables.GraceToken}.\"\n            else\n                match Grace.Types.PersonalAccessToken.tryParseToken normalized with\n                | Some _ -> Ok(Some normalized)\n                | None ->\n                    Error $\"GRACE_TOKEN accepts Grace PATs only (prefix {Grace.Types.PersonalAccessToken.TokenPrefix}). Auth0 access tokens are not valid here.\"\n\n    let private getTokenStoreNamespace (config: OidcCliConfig) =\n        let serverUri =\n            tryGetEnv Constants.EnvironmentVariables.GraceServerUri\n            |> Option.defaultValue String.Empty\n\n        $\"{config.Authority}|{config.Audience}|{config.ClientId}|{serverUri}\"\n            .Trim()\n\n    let private hashNamespace (value: string) =\n        use sha = SHA256.Create()\n        let bytes = Encoding.UTF8.GetBytes(value)\n        let hash = sha.ComputeHash(bytes)\n        Convert.ToHexString(hash).ToLowerInvariant()\n\n    let private tokenStoreCache = System.Collections.Concurrent.ConcurrentDictionary<string, Task<TokenStore>>()\n\n    let private createTokenStoreAsync (config: OidcCliConfig) =\n        task {\n            let cacheRoot = UserConfiguration.getUserGraceDirectory ()\n            let cacheDirectory = Path.Combine(cacheRoot, \"auth\")\n\n            Directory.CreateDirectory(cacheDirectory)\n            |> ignore\n\n            let key = getTokenStoreNamespace config |> hashNamespace\n            let fileName = $\"grace_auth_{key}.bin\"\n            let builder = StorageCreationPropertiesBuilder(fileName, cacheDirectory)\n\n            if OperatingSystem.IsMacOS() then\n                builder.WithMacKeyChain(\"Grace\", \"Grace.CLI.Auth\")\n                |> ignore\n            elif OperatingSystem.IsLinux() then\n                let attribute1 = KeyValuePair<string, string>(\"application\", \"grace\")\n                let attribute2 = KeyValuePair<string, string>(\"scope\", \"auth\")\n\n                builder.WithLinuxKeyring(\"com.grace.auth\", MsalCacheHelper.LinuxKeyRingDefaultCollection, \"Grace CLI Auth\", attribute1, attribute2)\n                |> ignore\n\n            let storageProperties = builder.Build()\n            let! helper = MsalCacheHelper.CreateAsync(storageProperties, null)\n\n            return\n                {\n                    Helper = helper\n                    StorageProperties = storageProperties\n                    LockFilePath = $\"{storageProperties.CacheFilePath}.lock\"\n                    InProcessLock = new SemaphoreSlim(1, 1)\n                }\n        }\n\n    let private getTokenStoreAsync (config: OidcCliConfig) =\n        let key = getTokenStoreNamespace config\n        tokenStoreCache.GetOrAdd(key, (fun _ -> createTokenStoreAsync config))\n\n    let private verifySecureStoreAsync (config: OidcCliConfig) =\n        task {\n            try\n                let! store = getTokenStoreAsync config\n                store.Helper.VerifyPersistence()\n                return Ok store\n            with\n            | ex -> return Error $\"Secure token storage is unavailable: {ex.Message}\"\n        }\n\n    let private withTokenLock (store: TokenStore) (action: unit -> Task<'T>) =\n        task {\n            do! store.InProcessLock.WaitAsync()\n\n            try\n                use _lock = new CrossPlatLock(store.LockFilePath, 100, 100)\n                return! action ()\n            finally\n                store.InProcessLock.Release() |> ignore\n        }\n\n    let private tryLoadTokenBundle (store: TokenStore) =\n        try\n            let data = store.Helper.LoadUnencryptedTokenCache()\n\n            if isNull data || data.Length = 0 then\n                None\n            else\n                let json = Encoding.UTF8.GetString(data)\n                let bundle = JsonSerializer.Deserialize<TokenBundle>(json, Constants.JsonSerializerOptions)\n                if obj.ReferenceEquals(bundle, null) then None else Some bundle\n        with\n        | _ -> None\n\n    let private saveTokenBundle (store: TokenStore) (bundle: TokenBundle) =\n        let json = JsonSerializer.Serialize(bundle, Constants.JsonSerializerOptions)\n        let data = Encoding.UTF8.GetBytes(json)\n        store.Helper.SaveUnencryptedTokenCache(data)\n\n    let private clearTokenBundle (store: TokenStore) = store.Helper.SaveUnencryptedTokenCache(Array.Empty<byte>())\n\n    let private tryReadString (root: JsonElement) (name: string) =\n        match root.TryGetProperty(name) with\n        | true, value when value.ValueKind = JsonValueKind.String ->\n            let strValue = value.GetString()\n            if String.IsNullOrWhiteSpace strValue then None else Some strValue\n        | _ -> None\n\n    let private tryReadInt (root: JsonElement) (name: string) =\n        match root.TryGetProperty(name) with\n        | true, value when value.ValueKind = JsonValueKind.Number ->\n            match value.TryGetInt32() with\n            | true, parsed -> Some parsed\n            | _ -> None\n        | _ -> None\n\n    let private parseTokenResponse (json: string) =\n        use document = JsonDocument.Parse(json)\n        let root = document.RootElement\n\n        match tryReadString root \"access_token\" with\n        | None -> Error \"Token response missing access_token.\"\n        | Some accessToken ->\n            Ok\n                {\n                    AccessToken = accessToken\n                    RefreshToken = tryReadString root \"refresh_token\"\n                    ExpiresIn = tryReadInt root \"expires_in\"\n                    Scope = tryReadString root \"scope\"\n                    TokenType = tryReadString root \"token_type\"\n                }\n\n    let private parseDeviceCodeResponse (json: string) =\n        use document = JsonDocument.Parse(json)\n        let root = document.RootElement\n\n        match tryReadString root \"device_code\", tryReadString root \"user_code\", tryReadString root \"verification_uri\", tryReadInt root \"expires_in\" with\n        | Some deviceCode, Some userCode, Some verificationUri, Some expiresIn ->\n            let verificationUriComplete = tryReadString root \"verification_uri_complete\"\n\n            let interval =\n                tryReadInt root \"interval\"\n                |> Option.defaultValue 5\n\n            Ok\n                {\n                    DeviceCode = deviceCode\n                    UserCode = userCode\n                    VerificationUri = verificationUri\n                    VerificationUriComplete = verificationUriComplete\n                    ExpiresIn = expiresIn\n                    IntervalSeconds = max 1 interval\n                }\n        | _ -> Error \"Device code response missing required fields.\"\n\n    let private tryReadOAuthError (json: string) =\n        try\n            use document = JsonDocument.Parse(json)\n            let root = document.RootElement\n            let error = tryReadString root \"error\"\n            let description = tryReadString root \"error_description\"\n\n            match error, description with\n            | Some e, Some d -> Some $\"{e}: {d}\"\n            | Some e, None -> Some e\n            | None, Some d -> Some d\n            | None, None -> None\n        with\n        | _ -> None\n\n    let private buildEndpoint (authority: string) (path: string) = $\"{authority.TrimEnd('/')}/{path.TrimStart('/')}\"\n\n    let private httpClient = new HttpClient()\n\n    let private tryCreateAbsoluteUri (url: string) =\n        match Uri.TryCreate(url, UriKind.Absolute) with\n        | true, uri when\n            uri.Scheme = Uri.UriSchemeHttps\n            || uri.Scheme = Uri.UriSchemeHttp\n            ->\n            Ok uri\n        | _ -> Error $\"Invalid OIDC endpoint URL: {url}. Check {Constants.EnvironmentVariables.GraceAuthOidcAuthority}.\"\n\n    let private postFormAsync (url: string) (formValues: (string * string) list) =\n        task {\n            let contentValues =\n                formValues\n                |> Seq.map (fun (key, value) -> KeyValuePair(key, value))\n\n            use content = new FormUrlEncodedContent(contentValues)\n\n            match tryCreateAbsoluteUri url with\n            | Error message -> return Error message\n            | Ok uri ->\n                let! response = httpClient.PostAsync(uri, content)\n                let! body = response.Content.ReadAsStringAsync()\n\n                if response.IsSuccessStatusCode then\n                    return Ok body\n                else\n                    let message = tryReadOAuthError body |> Option.defaultValue body\n                    return Error message\n        }\n\n    let private tryLaunchBrowser (url: string) =\n        try\n            let psi = ProcessStartInfo()\n            psi.FileName <- url\n            psi.UseShellExecute <- true\n            Process.Start(psi) |> ignore\n            Ok()\n        with\n        | ex -> Error ex.Message\n\n    let private generateBase64Url (bytes: int) =\n        let data = RandomNumberGenerator.GetBytes(bytes)\n\n        Convert\n            .ToBase64String(data)\n            .TrimEnd('=')\n            .Replace('+', '-')\n            .Replace('/', '_')\n\n    let private computeCodeChallenge (verifier: string) =\n        use sha = SHA256.Create()\n        let bytes = Encoding.ASCII.GetBytes(verifier)\n        let hash = sha.ComputeHash(bytes)\n\n        Convert\n            .ToBase64String(hash)\n            .TrimEnd('=')\n            .Replace('+', '-')\n            .Replace('/', '_')\n\n    let private tryGetJwtClaim (token: string) (claimType: string) =\n        try\n            let parts = token.Split('.')\n\n            if parts.Length < 2 then\n                None\n            else\n                let payload = parts[ 1 ].Replace('-', '+').Replace('_', '/')\n\n                let padded =\n                    payload\n                    + String.replicate ((4 - payload.Length % 4) % 4) \"=\"\n\n                let json = Encoding.UTF8.GetString(Convert.FromBase64String(padded))\n                use document = JsonDocument.Parse(json)\n                tryReadString document.RootElement claimType\n        with\n        | _ -> None\n\n    let private buildTokenBundle (config: OidcCliConfig) (tokenResponse: TokenResponse) =\n        let now = getCurrentInstant ()\n\n        let expiresIn =\n            tokenResponse.ExpiresIn\n            |> Option.defaultValue 3600\n\n        let expiresAt = now.Plus(Duration.FromSeconds(float expiresIn))\n\n        let issuer =\n            tryGetJwtClaim tokenResponse.AccessToken \"iss\"\n            |> Option.defaultValue config.Authority\n\n        let subject = tryGetJwtClaim tokenResponse.AccessToken \"sub\"\n\n        let scopes =\n            tokenResponse.Scope\n            |> Option.defaultValue (String.Join(\" \", config.Scopes))\n\n        {\n            RefreshToken =\n                tokenResponse.RefreshToken\n                |> Option.defaultValue String.Empty\n            AccessToken = tokenResponse.AccessToken\n            AccessTokenExpiresAt = expiresAt\n            Issuer = issuer\n            Audience = config.Audience\n            Scopes = scopes\n            Subject = subject\n            ClientId = config.ClientId\n            CreatedAt = now\n            UpdatedAt = now\n        }\n\n    let private requestTokenWithAuthorizationCodeAsync (config: OidcCliConfig) (redirectUri: string) (code: string) (codeVerifier: string) =\n        task {\n            let tokenEndpoint = buildEndpoint config.Authority \"oauth/token\"\n\n            let formValues =\n                [\n                    \"grant_type\", \"authorization_code\"\n                    \"client_id\", config.ClientId\n                    \"code\", code\n                    \"code_verifier\", codeVerifier\n                    \"redirect_uri\", redirectUri\n                ]\n\n            let! response = postFormAsync tokenEndpoint formValues\n\n            match response with\n            | Ok json -> return parseTokenResponse json\n            | Error message -> return Error message\n        }\n\n    let private requestDeviceCodeAsync (config: OidcCliConfig) =\n        task {\n            let endpoint = buildEndpoint config.Authority \"oauth/device/code\"\n\n            let formValues =\n                [\n                    \"client_id\", config.ClientId\n                    \"audience\", config.Audience\n                    \"scope\", String.Join(\" \", config.Scopes)\n                ]\n\n            let! response = postFormAsync endpoint formValues\n\n            match response with\n            | Ok json -> return parseDeviceCodeResponse json\n            | Error message -> return Error message\n        }\n\n    let private pollDeviceCodeAsync (config: OidcCliConfig) (deviceCode: DeviceCodeResponse) =\n        task {\n            let tokenEndpoint = buildEndpoint config.Authority \"oauth/token\"\n\n            let expiresAt =\n                getCurrentInstant()\n                    .Plus(Duration.FromSeconds(float deviceCode.ExpiresIn))\n\n            let mutable delaySeconds = deviceCode.IntervalSeconds\n            let mutable finished = false\n            let mutable finalResult = Error \"Device code expired. Please try again.\"\n\n            while not finished do\n                if getCurrentInstant () >= expiresAt then\n                    finished <- true\n                    finalResult <- Error \"Device code expired. Please try again.\"\n                else\n                    let formValues =\n                        [\n                            \"grant_type\", \"urn:ietf:params:oauth:grant-type:device_code\"\n                            \"device_code\", deviceCode.DeviceCode\n                            \"client_id\", config.ClientId\n                        ]\n\n                    let! response = postFormAsync tokenEndpoint formValues\n\n                    match response with\n                    | Ok json ->\n                        finished <- true\n                        finalResult <- parseTokenResponse json\n                    | Error message ->\n                        if message.StartsWith(\"authorization_pending\", StringComparison.OrdinalIgnoreCase) then\n                            do! Task.Delay(TimeSpan.FromSeconds(float delaySeconds))\n                        elif message.StartsWith(\"slow_down\", StringComparison.OrdinalIgnoreCase) then\n                            delaySeconds <- delaySeconds + 5\n                            do! Task.Delay(TimeSpan.FromSeconds(float delaySeconds))\n                        else\n                            finished <- true\n                            finalResult <- Error message\n\n            return finalResult\n        }\n\n    let private tryAcquireTokenWithPkceAsync (config: OidcCliConfig) (parseResult: ParseResult) =\n        task {\n            let redirectUri = $\"http://127.0.0.1:{config.RedirectPort}/callback\"\n            let listener = new HttpListener()\n\n            let startResult =\n                try\n                    listener.Prefixes.Add($\"http://127.0.0.1:{config.RedirectPort}/\")\n                    listener.Start()\n                    Ok()\n                with\n                | ex -> Error $\"Failed to listen on {redirectUri}: {ex.Message}\"\n\n            match startResult with\n            | Error message -> return Error message\n            | Ok () ->\n                try\n                    let state = generateBase64Url 16\n                    let codeVerifier = generateBase64Url 32\n                    let codeChallenge = computeCodeChallenge codeVerifier\n\n                    let authorizeEndpoint = buildEndpoint config.Authority \"authorize\"\n\n                    let query =\n                        [\n                            \"response_type\", \"code\"\n                            \"client_id\", config.ClientId\n                            \"redirect_uri\", redirectUri\n                            \"audience\", config.Audience\n                            \"scope\", String.Join(\" \", config.Scopes)\n                            \"code_challenge\", codeChallenge\n                            \"code_challenge_method\", \"S256\"\n                            \"state\", state\n                        ]\n                        |> List.map (fun (k, v) -> $\"{Uri.EscapeDataString(k)}={Uri.EscapeDataString(v)}\")\n                        |> String.concat \"&\"\n\n                    let url = $\"{authorizeEndpoint}?{query}\"\n\n                    match tryCreateAbsoluteUri url with\n                    | Error message -> return Error message\n                    | Ok _ ->\n                        match tryLaunchBrowser url with\n                        | Ok () -> ()\n                        | Error message ->\n                            AnsiConsole.MarkupLine($\"[{Colors.Important}]Open this URL in your browser to continue:[/] {Markup.Escape(url)}\")\n                            AnsiConsole.MarkupLine($\"[{Colors.Deemphasized}]Automatic launch failed: {Markup.Escape(message)}[/]\")\n\n                        let! context = listener.GetContextAsync()\n                        let request = context.Request\n                        let response = context.Response\n\n                        let writeResponse (message: string) =\n                            task {\n                                use writer = new StreamWriter(response.OutputStream)\n                                do! writer.WriteAsync(message)\n                                do! writer.FlushAsync()\n                                response.Close()\n                            }\n\n                        if\n                            request.Url.AbsolutePath.TrimEnd('/')\n                            <> \"/callback\"\n                        then\n                            do! writeResponse \"<html><body>Invalid callback path. You may close this window.</body></html>\"\n                            return Error \"Unexpected callback path.\"\n                        else\n                            let queryValues = request.QueryString\n                            let errorValue = queryValues[\"error\"]\n\n                            if not (String.IsNullOrWhiteSpace errorValue) then\n                                do! writeResponse \"<html><body>Authentication failed. You may close this window.</body></html>\"\n                                let description = queryValues[\"error_description\"]\n                                return Error $\"Authorization error: {errorValue} {description}\"\n                            else\n                                let code = queryValues[\"code\"]\n                                let returnedState = queryValues[\"state\"]\n\n                                if String.IsNullOrWhiteSpace code then\n                                    do! writeResponse \"<html><body>Authentication failed. You may close this window.</body></html>\"\n                                    return Error \"Authorization code missing from callback.\"\n                                elif not (String.Equals(state, returnedState, StringComparison.Ordinal)) then\n                                    do! writeResponse \"<html><body>Authentication failed. You may close this window.</body></html>\"\n                                    return Error \"Authorization state mismatch.\"\n                                else\n                                    do! writeResponse \"<html><body>Authentication complete. You may close this window.</body></html>\"\n                                    let! tokenResponse = requestTokenWithAuthorizationCodeAsync config redirectUri code codeVerifier\n\n                                    return tokenResponse\n                finally\n                    listener.Stop()\n        }\n\n    let private tryAcquireTokenWithDeviceFlowAsync (config: OidcCliConfig) (parseResult: ParseResult) =\n        task {\n            let! deviceResponse = requestDeviceCodeAsync config\n\n            match deviceResponse with\n            | Error message -> return Error message\n            | Ok deviceCode ->\n                if parseResult |> hasOutput then\n                    match deviceCode.VerificationUriComplete with\n                    | Some completeUrl -> AnsiConsole.MarkupLine($\"[{Colors.Important}]Complete sign-in:[/] {Markup.Escape(completeUrl)}\")\n                    | None ->\n                        AnsiConsole.MarkupLine($\"[{Colors.Important}]Open:[/] {Markup.Escape(deviceCode.VerificationUri)}\")\n                        AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Code:[/] {Markup.Escape(deviceCode.UserCode)}\")\n\n                return! pollDeviceCodeAsync config deviceCode\n        }\n\n    let private applyRefreshToken (bundle: TokenBundle) (refreshed: TokenResponse) (now: Instant) : TokenBundle =\n        let expiresIn = refreshed.ExpiresIn |> Option.defaultValue 3600\n        let expiresAt = now.Plus(Duration.FromSeconds(float expiresIn))\n\n        let refreshToken =\n            refreshed.RefreshToken\n            |> Option.defaultValue bundle.RefreshToken\n\n        let scopes =\n            refreshed.Scope\n            |> Option.defaultValue bundle.Scopes\n\n        let issuer =\n            tryGetJwtClaim refreshed.AccessToken \"iss\"\n            |> Option.defaultValue bundle.Issuer\n\n        let subject =\n            tryGetJwtClaim refreshed.AccessToken \"sub\"\n            |> Option.orElse bundle.Subject\n\n        { bundle with\n            RefreshToken = refreshToken\n            AccessToken = refreshed.AccessToken\n            AccessTokenExpiresAt = expiresAt\n            Issuer = issuer\n            Scopes = scopes\n            Subject = subject\n            UpdatedAt = now\n        }\n\n    let private tryRefreshTokenAsync (config: OidcCliConfig) (bundle: TokenBundle) =\n        task {\n            if String.IsNullOrWhiteSpace bundle.RefreshToken then\n                return Error \"Refresh token missing. Run 'grace auth login' again.\"\n            else\n                let endpoint = buildEndpoint config.Authority \"oauth/token\"\n\n                let formValues =\n                    [\n                        \"grant_type\", \"refresh_token\"\n                        \"client_id\", config.ClientId\n                        \"refresh_token\", bundle.RefreshToken\n                        \"audience\", config.Audience\n                    ]\n\n                let! response = postFormAsync endpoint formValues\n\n                match response with\n                | Error message -> return Error message\n                | Ok json ->\n                    match parseTokenResponse json with\n                    | Error message -> return Error message\n                    | Ok refreshed ->\n                        let now = getCurrentInstant ()\n                        let updated = applyRefreshToken bundle refreshed now\n                        return Ok updated\n        }\n\n    let private safetyWindow = Duration.FromSeconds(90.0)\n\n    let private tryGetInteractiveTokenAsync (config: OidcCliConfig) =\n        task {\n            let! storeResult = verifySecureStoreAsync config\n\n            match storeResult with\n            | Error message -> return Error message\n            | Ok store ->\n                return!\n                    withTokenLock store (fun () ->\n                        task {\n                            match tryLoadTokenBundle store with\n                            | None -> return Ok None\n                            | Some bundle ->\n                                let now = getCurrentInstant ()\n\n                                if bundle.AccessTokenExpiresAt > now.Plus(safetyWindow) then\n                                    return Ok(Some bundle.AccessToken)\n                                else\n                                    let! refreshResult = tryRefreshTokenAsync config bundle\n\n                                    match refreshResult with\n                                    | Ok updated ->\n                                        saveTokenBundle store updated\n                                        return Ok(Some updated.AccessToken)\n                                    | Error message ->\n                                        clearTokenBundle store\n                                        return Error message\n                        })\n        }\n\n    let tryGetAccessToken () =\n        task {\n            match tryGetGraceTokenFromEnv () with\n            | Error message -> return Error message\n            | Ok (Some token) -> return Ok(Some token)\n            | Ok None ->\n                match tryGetEnv Constants.EnvironmentVariables.GraceTokenFile with\n                | Some _ ->\n                    return\n                        Error\n                            $\"Local token files are no longer supported. Remove {Constants.EnvironmentVariables.GraceTokenFile} and set {Constants.EnvironmentVariables.GraceToken} instead.\"\n                | None ->\n                    match tryGetOidcM2mConfig () with\n                    | Some m2mConfig ->\n                        let endpoint = buildEndpoint m2mConfig.Authority \"oauth/token\"\n\n                        let formValues =\n                            [\n                                \"grant_type\", \"client_credentials\"\n                                \"client_id\", m2mConfig.ClientId\n                                \"client_secret\", m2mConfig.ClientSecret\n                                \"audience\", m2mConfig.Audience\n                            ]\n                            |> fun values ->\n                                if List.isEmpty m2mConfig.Scopes then\n                                    values\n                                else\n                                    values\n                                    @ [\n                                        \"scope\", String.Join(\" \", m2mConfig.Scopes)\n                                    ]\n\n                        let! response = postFormAsync endpoint formValues\n\n                        match response with\n                        | Error message -> return Error message\n                        | Ok json ->\n                            match parseTokenResponse json with\n                            | Error message -> return Error message\n                            | Ok token -> return Ok(Some token.AccessToken)\n                    | None ->\n                        let correlationId = ensureNonEmptyCorrelationId String.Empty\n                        let! cliConfigResult = tryGetOidcCliConfig correlationId\n\n                        match cliConfigResult with\n                        | Ok None ->\n                            return\n                                Error\n                                    $\"Authentication is not configured. Set {Constants.EnvironmentVariables.GraceAuthOidcAuthority}, {Constants.EnvironmentVariables.GraceAuthOidcAudience}, and {Constants.EnvironmentVariables.GraceAuthOidcCliClientId} (or provide GRACE_TOKEN / M2M credentials).\"\n                        | Ok (Some cliConfig) ->\n                            let! tokenResult = tryGetInteractiveTokenAsync cliConfig\n                            return tokenResult\n                        | Error error -> return Error error.Error\n        }\n\n    let private tryGetAccessTokenForSdk () =\n        task {\n            let! result = tryGetAccessToken ()\n\n            match result with\n            | Ok token -> return token\n            | Error _ -> return None\n        }\n\n    let configureSdkAuth () = Grace.SDK.Auth.setTokenProvider (fun () -> tryGetAccessTokenForSdk ())\n\n    let private parseDurationSeconds (value: string) =\n        if String.IsNullOrWhiteSpace value then\n            Error \"Expires-in value is required.\"\n        else\n            let trimmed = value.Trim()\n\n            if trimmed.Length < 2 then\n                Error \"Expires-in must include a unit suffix: s, m, h, or d.\"\n            else\n                let unitChar = Char.ToLowerInvariant(trimmed[trimmed.Length - 1])\n                let amountPart = trimmed.Substring(0, trimmed.Length - 1)\n\n                match Int64.TryParse(amountPart) with\n                | true, amount when amount > 0L ->\n                    let seconds =\n                        match unitChar with\n                        | 's' -> Some amount\n                        | 'm' -> Some(amount * 60L)\n                        | 'h' -> Some(amount * 3600L)\n                        | 'd' -> Some(amount * 86400L)\n                        | _ -> None\n\n                    match seconds with\n                    | Some value -> Ok value\n                    | None -> Error \"Expires-in must end with s, m, h, or d.\"\n                | _ -> Error \"Expires-in must start with a positive integer.\"\n\n    let private formatInstantOption (instant: NodaTime.Instant option) =\n        match instant with\n        | None -> \"Never\"\n        | Some value -> instantToLocalTime value\n\n    module private LoginOptions =\n        let auth =\n            (new Option<string>(\"--auth\", Required = false, Description = \"Authentication flow: pkce (browser) or device.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong([| \"pkce\"; \"device\" |])\n\n    module private TokenOptions =\n        let name =\n            new Option<string>(\"--name\", Required = true, Description = \"A friendly name for the personal access token.\", Arity = ArgumentArity.ExactlyOne)\n\n        let expiresIn =\n            new Option<string>(\n                \"--expires-in\",\n                Required = false,\n                Description = \"Token lifetime with unit suffix: 30d, 12h, 60m, or 3600s.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let noExpiry =\n            new Option<bool>(\n                \"--no-expiry\",\n                Required = false,\n                Description = \"Create a token with no expiry (if server policy allows).\",\n                Arity = ArgumentArity.Zero\n            )\n\n        let store =\n            new Option<bool>(\n                \"--store\",\n                Required = false,\n                Description = \"Deprecated: local token storage is disabled. Use GRACE_TOKEN instead.\",\n                Arity = ArgumentArity.Zero\n            )\n\n        let includeRevoked =\n            new Option<bool>(\"--include-revoked\", Required = false, Description = \"Include revoked tokens in the list.\", Arity = ArgumentArity.Zero)\n\n        let includeExpired =\n            new Option<bool>(\"--include-expired\", Required = false, Description = \"Include expired tokens in the list.\", Arity = ArgumentArity.Zero)\n\n        let all = new Option<bool>(\"--all\", Required = false, Description = \"Include revoked and expired tokens in the list.\", Arity = ArgumentArity.Zero)\n\n        let tokenId = new Argument<string>(\"token-id\", Description = \"Token id (GUID).\")\n\n        let token =\n            new Option<string>(\n                \"--token\",\n                Required = false,\n                Description = \"Personal access token (local storage is disabled).\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let stdin =\n            new Option<bool>(\n                \"--stdin\",\n                Required = false,\n                Description = \"Read the token value from standard input (local storage is disabled).\",\n                Arity = ArgumentArity.Zero\n            )\n\n    let private authDevelopmentGuidance = \"During development, TestAuth may be available. See docs/Authentication.md for details.\"\n\n    let private authenticationRequiredMessage = $\"Authentication required. Run 'grace auth login' and try again. {authDevelopmentGuidance}\"\n\n    let private addAuthDevelopmentGuidance (message: string) =\n        if String.IsNullOrWhiteSpace message then\n            authDevelopmentGuidance\n        else\n            $\"{message} {authDevelopmentGuidance}\"\n\n    let ensureAccessToken (parseResult: ParseResult) =\n        task {\n            let correlationId = parseResult |> getCorrelationId\n            let! tokenResult = tryGetAccessToken ()\n\n            match tokenResult with\n            | Ok (Some _) -> return ()\n            | Ok None ->\n                Error(GraceError.Create authenticationRequiredMessage correlationId)\n                |> renderOutput parseResult\n                |> ignore\n\n                raise (OperationCanceledException())\n            | Error message ->\n                Error(GraceError.Create (addAuthDevelopmentGuidance message) correlationId)\n                |> renderOutput parseResult\n                |> ignore\n\n                raise (OperationCanceledException())\n        }\n\n    type Login() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                let! cliConfigResult = tryGetOidcCliConfig correlationId\n\n                match cliConfigResult with\n                | Ok None ->\n                    return\n                        Error(\n                            GraceError.Create\n                                $\"Authentication is not configured. Set {Constants.EnvironmentVariables.GraceAuthOidcAuthority}, {Constants.EnvironmentVariables.GraceAuthOidcAudience}, and {Constants.EnvironmentVariables.GraceAuthOidcCliClientId}.\"\n                                correlationId\n                        )\n                        |> renderOutput parseResult\n                | Ok (Some config) ->\n                    let desiredAuth =\n                        let raw = parseResult.GetValue(LoginOptions.auth)\n\n                        if String.IsNullOrWhiteSpace raw then\n                            None\n                        elif raw.Equals(\"device\", StringComparison.OrdinalIgnoreCase) then\n                            Some LoginMode.Device\n                        else\n                            Some LoginMode.Pkce\n\n                    let! storeResult = verifySecureStoreAsync config\n\n                    match storeResult with\n                    | Error message ->\n                        return\n                            Error(GraceError.Create $\"{message} Use GRACE_TOKEN or configure Auth0 client credentials (M2M).\" correlationId)\n                            |> renderOutput parseResult\n                    | Ok store ->\n                        let! tokenResult =\n                            match desiredAuth with\n                            | Some LoginMode.Device -> tryAcquireTokenWithDeviceFlowAsync config parseResult\n                            | Some LoginMode.Pkce -> tryAcquireTokenWithPkceAsync config parseResult\n                            | None ->\n                                task {\n                                    let! pkceResult = tryAcquireTokenWithPkceAsync config parseResult\n\n                                    match pkceResult with\n                                    | Ok _ -> return pkceResult\n                                    | Error _ -> return! tryAcquireTokenWithDeviceFlowAsync config parseResult\n                                }\n\n                        match tokenResult with\n                        | Error message ->\n                            return\n                                Error(GraceError.Create message correlationId)\n                                |> renderOutput parseResult\n                        | Ok response ->\n                            let refreshToken =\n                                response.RefreshToken\n                                |> Option.defaultValue String.Empty\n\n                            if String.IsNullOrWhiteSpace refreshToken then\n                                return\n                                    Error(\n                                        GraceError.Create\n                                            \"Refresh token missing. Ensure offline_access scope and refresh token rotation are enabled.\"\n                                            correlationId\n                                    )\n                                    |> renderOutput parseResult\n                            else\n                                let bundle = { buildTokenBundle config response with RefreshToken = refreshToken }\n\n                                do! withTokenLock store (fun () -> task { saveTokenBundle store bundle })\n\n                                if parseResult |> hasOutput then\n                                    let subject = bundle.Subject |> Option.defaultValue \"unknown\"\n                                    AnsiConsole.MarkupLine($\"[{Colors.Important}]Signed in.[/] {Markup.Escape(subject)}\")\n\n                                return\n                                    Ok(GraceReturnValue.Create \"Authenticated.\" correlationId)\n                                    |> renderOutput parseResult\n                | Error error -> return Error error |> renderOutput parseResult\n            }\n\n    type Status() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                let graceTokenResult = tryGetGraceTokenFromEnv ()\n\n                let graceTokenPresent =\n                    match graceTokenResult with\n                    | Ok (Some _) -> true\n                    | Ok None -> false\n                    | Error _ -> true\n\n                let graceTokenValid =\n                    match graceTokenResult with\n                    | Ok (Some _) -> true\n                    | _ -> false\n\n                let graceTokenError =\n                    match graceTokenResult with\n                    | Error message -> Some message\n                    | _ -> None\n\n                let m2mConfigured = tryGetOidcM2mConfig () |> Option.isSome\n\n                let! cliConfigResult = tryGetOidcCliConfig correlationId\n                let mutable configError: string option = None\n\n                let cliConfig =\n                    match cliConfigResult with\n                    | Ok value -> value\n                    | Error error ->\n                        configError <- Some error.Error\n                        None\n\n                let mutable interactiveBundle: TokenBundle option = None\n                let mutable secureStoreError: string option = None\n\n                match cliConfig with\n                | None -> ()\n                | Some config ->\n                    let! storeResult = verifySecureStoreAsync config\n\n                    match storeResult with\n                    | Error message -> secureStoreError <- Some message\n                    | Ok store ->\n                        let! bundleOpt = withTokenLock store (fun () -> task { return tryLoadTokenBundle store })\n\n                        interactiveBundle <- bundleOpt\n\n                let interactiveConfigured = cliConfig |> Option.isSome\n                let interactiveTokenPresent = interactiveBundle |> Option.isSome\n\n                let interactiveExpiresAt =\n                    interactiveBundle\n                    |> Option.map (fun bundle -> bundle.AccessTokenExpiresAt)\n\n                let interactiveSubject =\n                    interactiveBundle\n                    |> Option.bind (fun bundle -> bundle.Subject)\n\n                let activeSource =\n                    if graceTokenValid then\n                        \"Environment (GRACE_TOKEN)\"\n                    elif graceTokenError.IsSome then\n                        \"Environment (GRACE_TOKEN invalid)\"\n                    elif m2mConfigured then\n                        \"M2M (client credentials)\"\n                    elif interactiveConfigured then\n                        if secureStoreError.IsSome then \"Interactive (secure storage unavailable)\"\n                        elif interactiveTokenPresent then \"Interactive (cached token)\"\n                        else \"Interactive (no cached token)\"\n                    else\n                        \"None\"\n\n                if parseResult |> hasOutput then\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]GRACE_TOKEN:[/] {graceTokenPresent}\")\n\n                    if graceTokenError.IsSome then\n                        AnsiConsole.MarkupLine($\"[{Colors.Important}]GRACE_TOKEN error:[/] {Markup.Escape(graceTokenError.Value)}\")\n\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]M2M configured:[/] {m2mConfigured}\")\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Interactive configured:[/] {interactiveConfigured}\")\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Interactive token:[/] {interactiveTokenPresent}\")\n\n                    match interactiveExpiresAt with\n                    | Some expiresAt ->\n                        AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Access token expires:[/] {Markup.Escape(formatInstantOption (Some expiresAt))}\")\n                    | None -> ()\n\n                    match interactiveSubject with\n                    | Some subject -> AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Subject:[/] {Markup.Escape(subject)}\")\n                    | None -> ()\n\n                    match secureStoreError with\n                    | Some message -> AnsiConsole.MarkupLine($\"[{Colors.Important}]Secure storage:[/] {Markup.Escape(message)}\")\n                    | None -> ()\n\n                    match configError with\n                    | Some message -> AnsiConsole.MarkupLine($\"[{Colors.Important}]Auth config:[/] {Markup.Escape(message)}\")\n                    | None -> ()\n\n                    AnsiConsole.MarkupLine($\"[{Colors.Important}]Active source:[/] {Markup.Escape(activeSource)}\")\n\n                return\n                    Ok(GraceReturnValue.Create activeSource correlationId)\n                    |> renderOutput parseResult\n            }\n\n    type Logout() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                let! cliConfigResult = tryGetOidcCliConfig correlationId\n\n                match cliConfigResult with\n                | Ok None ->\n                    return\n                        Error(GraceError.Create \"Interactive authentication is not configured.\" correlationId)\n                        |> renderOutput parseResult\n                | Ok (Some config) ->\n                    let! storeResult = verifySecureStoreAsync config\n\n                    match storeResult with\n                    | Error message ->\n                        return\n                            Error(GraceError.Create message correlationId)\n                            |> renderOutput parseResult\n                    | Ok store ->\n                        do! withTokenLock store (fun () -> task { clearTokenBundle store })\n\n                        if parseResult |> hasOutput then\n                            AnsiConsole.MarkupLine($\"[{Colors.Important}]Signed out.[/]\")\n\n                        return\n                            Ok(GraceReturnValue.Create \"Signed out.\" correlationId)\n                            |> renderOutput parseResult\n                | Error error -> return Error error |> renderOutput parseResult\n            }\n\n    type WhoAmI() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n                let parameters = CommonParameters(CorrelationId = correlationId)\n                let! result = Grace.SDK.Common.getServer<CommonParameters, AuthInfo> (parameters, \"auth/me\")\n\n                match result with\n                | Ok graceReturnValue ->\n                    if parseResult |> hasOutput then\n                        AnsiConsole.MarkupLine($\"[{Colors.Important}]Grace user id: {Markup.Escape(graceReturnValue.ReturnValue.GraceUserId)}[/]\")\n\n                        if not\n                           <| List.isEmpty graceReturnValue.ReturnValue.Claims then\n                            let claimList = String.Join(\", \", graceReturnValue.ReturnValue.Claims)\n                            AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Claims:[/] {Markup.Escape(claimList)}\")\n\n                    return Ok graceReturnValue |> renderOutput parseResult\n                | Error error -> return Error error |> renderOutput parseResult\n            }\n\n    type TokenCreate() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                do! ensureAccessToken parseResult\n\n                let tokenName = parseResult.GetValue(TokenOptions.name)\n                let expiresInRaw = parseResult.GetValue(TokenOptions.expiresIn)\n                let noExpiry = parseResult.GetValue(TokenOptions.noExpiry)\n                let store = parseResult.GetValue(TokenOptions.store)\n\n                if store then\n                    return\n                        Error(GraceError.Create $\"Local token storage is disabled. Set {Constants.EnvironmentVariables.GraceToken} instead.\" correlationId)\n                        |> renderOutput parseResult\n                else\n                    let expiresInResult =\n                        if String.IsNullOrWhiteSpace expiresInRaw then\n                            Ok 0L\n                        else\n                            parseDurationSeconds expiresInRaw\n\n                    match expiresInResult with\n                    | Error message ->\n                        return\n                            Error(GraceError.Create message correlationId)\n                            |> renderOutput parseResult\n                    | Ok expiresInSeconds ->\n                        let parameters = Grace.Shared.Parameters.Auth.CreatePersonalAccessTokenParameters()\n                        parameters.CorrelationId <- correlationId\n                        parameters.TokenName <- tokenName\n                        parameters.ExpiresInSeconds <- expiresInSeconds\n                        parameters.NoExpiry <- noExpiry\n\n                        let! result = Grace.SDK.PersonalAccessToken.Create parameters\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            let created = graceReturnValue.ReturnValue\n                            let storedPath: string option = None\n\n                            if parseResult |> hasOutput then\n                                let summary = created.Summary\n                                let expiresText = formatInstantOption summary.ExpiresAt\n\n                                AnsiConsole.MarkupLine($\"[{Colors.Important}]Token created.[/]\")\n                                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Name:[/] {Markup.Escape(summary.Name)}\")\n                                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Token Id:[/] {summary.TokenId}\")\n                                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Expires:[/] {Markup.Escape(expiresText)}\")\n                                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Token:[/] {Markup.Escape(created.Token)}\")\n                                AnsiConsole.MarkupLine($\"[{Colors.Deemphasized}]This token will not be shown again.[/]\")\n\n                                match storedPath with\n                                | Some path -> AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Stored token at:[/] {Markup.Escape(path)}\")\n                                | None -> ()\n\n                            return Ok graceReturnValue |> renderOutput parseResult\n                        | Error error -> return Error error |> renderOutput parseResult\n            }\n\n    let private renderTokenList (_parseResult: ParseResult) (tokens: Grace.Types.PersonalAccessToken.PersonalAccessTokenSummary list) : unit =\n        let table = Table(Border = TableBorder.Rounded)\n        table.AddColumn(\"Name\") |> ignore\n        table.AddColumn(\"TokenId\") |> ignore\n        table.AddColumn(\"Created\") |> ignore\n        table.AddColumn(\"Expires\") |> ignore\n        table.AddColumn(\"Last Used\") |> ignore\n        table.AddColumn(\"Revoked\") |> ignore\n\n        tokens\n        |> List.iter (fun token ->\n            let created = instantToLocalTime token.CreatedAt\n            let expiresText = formatInstantOption token.ExpiresAt\n            let lastUsed = formatInstantOption token.LastUsedAt\n            let revoked = formatInstantOption token.RevokedAt\n\n            table.AddRow(\n                Markup.Escape(token.Name),\n                token.TokenId.ToString(),\n                Markup.Escape(created),\n                Markup.Escape(expiresText),\n                Markup.Escape(lastUsed),\n                Markup.Escape(revoked)\n            )\n            |> ignore)\n\n        AnsiConsole.Write(table)\n\n    type TokenList() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                do! ensureAccessToken parseResult\n\n                let includeRevoked = parseResult.GetValue(TokenOptions.includeRevoked)\n                let includeExpired = parseResult.GetValue(TokenOptions.includeExpired)\n                let includeAll = parseResult.GetValue(TokenOptions.all)\n\n                let parameters = Grace.Shared.Parameters.Auth.ListPersonalAccessTokensParameters()\n                parameters.CorrelationId <- correlationId\n                parameters.IncludeRevoked <- includeRevoked || includeAll\n                parameters.IncludeExpired <- includeExpired || includeAll\n\n                let! result = Grace.SDK.PersonalAccessToken.List parameters\n\n                match result with\n                | Ok graceReturnValue ->\n                    if parseResult |> hasOutput then\n                        renderTokenList parseResult graceReturnValue.ReturnValue\n\n                    return Ok graceReturnValue |> renderOutput parseResult\n                | Error error -> return Error error |> renderOutput parseResult\n            }\n\n    type TokenRevoke() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                do! ensureAccessToken parseResult\n\n                let tokenIdRaw = parseResult.GetValue(TokenOptions.tokenId)\n\n                match Guid.TryParse(tokenIdRaw) with\n                | false, _ ->\n                    return\n                        Error(GraceError.Create \"Token id must be a valid GUID.\" correlationId)\n                        |> renderOutput parseResult\n                | true, tokenId ->\n                    let parameters = Grace.Shared.Parameters.Auth.RevokePersonalAccessTokenParameters()\n                    parameters.CorrelationId <- correlationId\n                    parameters.TokenId <- tokenId\n\n                    let! result = Grace.SDK.PersonalAccessToken.Revoke parameters\n\n                    match result with\n                    | Ok graceReturnValue ->\n                        if parseResult |> hasOutput then\n                            AnsiConsole.MarkupLine($\"[{Colors.Important}]Token revoked.[/]\")\n\n                        return Ok graceReturnValue |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n            }\n\n    type TokenSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                return\n                    Error(GraceError.Create $\"Local token storage is disabled. Set {Constants.EnvironmentVariables.GraceToken} for a PAT.\" correlationId)\n                    |> renderOutput parseResult\n            }\n\n    type TokenClear() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                return\n                    Error(GraceError.Create $\"Local token storage is disabled. Set {Constants.EnvironmentVariables.GraceToken} for a PAT.\" correlationId)\n                    |> renderOutput parseResult\n            }\n\n    type TokenStatus() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                let correlationId = parseResult |> getCorrelationId\n\n                let graceTokenResult = tryGetGraceTokenFromEnv ()\n\n                let graceTokenPresent =\n                    match graceTokenResult with\n                    | Ok (Some _) -> true\n                    | Ok None -> false\n                    | Error _ -> true\n\n                let graceTokenValid =\n                    match graceTokenResult with\n                    | Ok (Some _) -> true\n                    | _ -> false\n\n                let graceTokenError =\n                    match graceTokenResult with\n                    | Error message -> Some message\n                    | _ -> None\n\n                if parseResult |> hasOutput then\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]GRACE_TOKEN:[/] {graceTokenPresent}\")\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]GRACE_TOKEN valid:[/] {graceTokenValid}\")\n\n                    match graceTokenError with\n                    | Some message -> AnsiConsole.MarkupLine($\"[{Colors.Important}]GRACE_TOKEN error:[/] {Markup.Escape(message)}\")\n                    | None -> ()\n\n                    AnsiConsole.MarkupLine($\"[{Colors.Deemphasized}]Local token storage is disabled.[/]\")\n\n                return\n                    Ok(GraceReturnValue.Create \"Token status.\" correlationId)\n                    |> renderOutput parseResult\n            }\n\n    let Build =\n        let authCommand = new Command(\"auth\", Description = \"Authenticate with Grace.\")\n\n        let loginCommand = new Command(\"login\", Description = \"Sign in with Auth0 (PKCE or device flow).\")\n        loginCommand.Options.Add(LoginOptions.auth)\n        loginCommand.Action <- new Login()\n        authCommand.Subcommands.Add(loginCommand)\n\n        let statusCommand = new Command(\"status\", Description = \"Show cached login status.\")\n        statusCommand.Action <- new Status()\n        authCommand.Subcommands.Add(statusCommand)\n\n        let logoutCommand = new Command(\"logout\", Description = \"Sign out and clear cached credentials.\")\n        logoutCommand.Action <- new Logout()\n        authCommand.Subcommands.Add(logoutCommand)\n\n        let whoamiCommand = new Command(\"whoami\", Description = \"Show the authenticated Grace principal.\")\n        whoamiCommand.Action <- new WhoAmI()\n        authCommand.Subcommands.Add(whoamiCommand)\n\n        let tokenCommand = new Command(\"token\", Description = \"Manage personal access tokens.\")\n\n        let tokenCreateCommand = new Command(\"create\", Description = \"Create a personal access token.\")\n        tokenCreateCommand.Options.Add(TokenOptions.name)\n        tokenCreateCommand.Options.Add(TokenOptions.expiresIn)\n        tokenCreateCommand.Options.Add(TokenOptions.noExpiry)\n        tokenCreateCommand.Options.Add(TokenOptions.store)\n        tokenCreateCommand.Action <- new TokenCreate()\n        tokenCommand.Subcommands.Add(tokenCreateCommand)\n\n        let tokenListCommand = new Command(\"list\", Description = \"List personal access tokens.\")\n        tokenListCommand.Options.Add(TokenOptions.includeRevoked)\n        tokenListCommand.Options.Add(TokenOptions.includeExpired)\n        tokenListCommand.Options.Add(TokenOptions.all)\n        tokenListCommand.Action <- new TokenList()\n        tokenCommand.Subcommands.Add(tokenListCommand)\n\n        let tokenRevokeCommand = new Command(\"revoke\", Description = \"Revoke a personal access token.\")\n        tokenRevokeCommand.Arguments.Add(TokenOptions.tokenId)\n        tokenRevokeCommand.Action <- new TokenRevoke()\n        tokenCommand.Subcommands.Add(tokenRevokeCommand)\n\n        let tokenSetCommand = new Command(\"set\", Description = \"Store a personal access token locally (disabled).\")\n        tokenSetCommand.Options.Add(TokenOptions.token)\n        tokenSetCommand.Options.Add(TokenOptions.stdin)\n        tokenSetCommand.Action <- new TokenSet()\n        tokenCommand.Subcommands.Add(tokenSetCommand)\n\n        let tokenClearCommand = new Command(\"clear\", Description = \"Clear the local personal access token (disabled).\")\n        tokenClearCommand.Action <- new TokenClear()\n        tokenCommand.Subcommands.Add(tokenClearCommand)\n\n        let tokenStatusCommand = new Command(\"status\", Description = \"Show personal access token status.\")\n        tokenStatusCommand.Action <- new TokenStatus()\n        tokenCommand.Subcommands.Add(tokenStatusCommand)\n\n        authCommand.Subcommands.Add(tokenCommand)\n\n        authCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Branch.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Client.Theme\nopen Grace.Shared.Parameters.Branch\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Services\nopen Grace.Shared.Resources\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Branch\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen NodaTime\nopen NodaTime.TimeZones\nopen Spectre.Console\nopen Spectre.Console.Json\nopen Spectre.Console.Rendering\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Globalization\nopen System.IO\nopen System.IO.Enumeration\nopen System.Linq\nopen System.Threading\nopen System.Security.Cryptography\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\nopen Grace.Shared.Parameters\nopen Grace.Shared.Client\nopen System.Text.RegularExpressions\nopen System.CommandLine.Completions\n\nmodule Branch =\n\n    type CommonParameters() =\n        inherit ParameterBase()\n        member val public BranchId: string = String.Empty with get, set\n        member val public BranchName: string = String.Empty with get, set\n        member val public OwnerId: string = String.Empty with get, set\n        member val public OwnerName: string = String.Empty with get, set\n        member val public OrganizationId: string = String.Empty with get, set\n        member val public OrganizationName: string = String.Empty with get, set\n        member val public RepositoryId: string = String.Empty with get, set\n        member val public RepositoryName: string = String.Empty with get, set\n\n    module private Options =\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branchId =\n            new Option<BranchId>(\n                OptionName.BranchId,\n                [| \"-i\" |],\n                Required = false,\n                Description = \"The branch's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let branchName =\n            new Option<String>(\n                OptionName.BranchName,\n                [| \"-b\" |],\n                Required = false,\n                Description = \"The name of the branch. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branchNameRequired =\n            new Option<String>(OptionName.BranchName, [| \"-b\" |], Required = true, Description = \"The name of the branch.\", Arity = ArgumentArity.ExactlyOne)\n\n        let parentBranchId =\n            new Option<Guid>(\n                OptionName.ParentBranchId,\n                [||],\n                Required = false,\n                Description = \"The parent branch's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let parentBranchName =\n            new Option<String>(\n                OptionName.ParentBranchName,\n                [||],\n                Required = false,\n                Description = \"The name of the parent branch. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> String.Empty)\n            )\n\n        let newName = new Option<String>(OptionName.NewName, Required = true, Description = \"The new name of the branch.\", Arity = ArgumentArity.ExactlyOne)\n\n        let message =\n            new Option<String>(\n                OptionName.Message,\n                [| \"-m\" |],\n                Required = false,\n                Description = \"The text to store with this reference.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let messageRequired =\n            new Option<String>(\n                OptionName.Message,\n                [| \"-m\" |],\n                Required = true,\n                Description = \"The text to store with this reference.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let referenceType =\n            (new Option<String>(OptionName.ReferenceType, Required = false, Description = \"The type of reference.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(listCases<ReferenceType> ())\n\n        let doNotSwitch =\n            new Option<bool>(\n                OptionName.DoNotSwitch,\n                Required = false,\n                Description = \"Do not switch your current branch to the new branch after it is created. By default, the new branch becomes the current branch.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let fullSha =\n            new Option<bool>(\n                OptionName.FullSha,\n                Required = false,\n                Description = \"Show the full SHA-256 value in output.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let maxCount =\n            new Option<int>(\n                OptionName.MaxCount,\n                Required = false,\n                Description = \"The maximum number of results to return.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> 30)\n            )\n\n        let referenceId =\n            new Option<ReferenceId>(OptionName.ReferenceId, [||], Required = false, Description = \"The reference ID <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n        let sha256Hash =\n            new Option<String>(\n                OptionName.Sha256Hash,\n                [||],\n                Required = false,\n                Description = \"The full or partial SHA-256 hash value of the version.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let enabled =\n            new Option<bool>(\n                OptionName.Enabled,\n                [||],\n                Required = false,\n                Description = \"True to enable the feature; false to disable it.\",\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let includeDeleted =\n            new Option<bool>(OptionName.IncludeDeleted, [| \"-d\" |], Required = false, Description = \"Include deleted branches in the result. [default: false]\")\n\n        let showEvents =\n            new Option<bool>(OptionName.ShowEvents, [| \"-e\" |], Required = false, Description = \"Include actor events in the result. [default: false]\")\n\n        let initialPermissions =\n            new Option<ReferenceType array>(\n                OptionName.InitialPermissions,\n                Required = false,\n                Description = \"A list of reference types allowed in this branch.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory =\n                    (fun _ ->\n                        [|\n                            Commit\n                            Checkpoint\n                            Save\n                            Tag\n                            External\n                        |])\n            )\n\n        let reassignChildBranches =\n            new Option<bool>(\n                OptionName.ReassignChildBranches,\n                [| \"--reassign-child-branches\" |],\n                Required = false,\n                Description = \"Reassign child branches to a new parent when deleting a branch.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let newParentBranchId =\n            new Option<String>(\n                OptionName.NewParentBranchId,\n                [| \"--new-parent-branch-id\" |],\n                Required = false,\n                Description = \"The new parent branch's ID <Guid> for reassigning children.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let newParentBranchName =\n            new Option<String>(\n                OptionName.NewParentBranchName,\n                [| \"--new-parent-branch-name\" |],\n                Required = false,\n                Description = \"The name of the new parent branch for reassigning children.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let force =\n            new Option<bool>(\n                OptionName.Force,\n                [| \"-f\"; \"--force\" |],\n                Required = false,\n                Description = \"Force delete all child branches before deleting this branch.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let individual =\n            new Option<bool>(\n                OptionName.Individual,\n                [| \"--individual\" |],\n                Required = false,\n                Description = \"Force an individual promotion, bypassing any promotion group on a Hybrid branch.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let promotionMode =\n            (new Option<String>(\n                \"--promotion-mode\",\n                [| \"-pm\" |],\n                Required = true,\n                Description = \"The promotion mode for the branch: IndividualOnly, GroupOnly, or Hybrid.\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<BranchPromotionMode> ())\n\n        let toBranchId =\n            new Option<BranchId>(\n                OptionName.ToBranchId,\n                [| \"-d\" |],\n                Required = false,\n                Description = \"The ID of the branch to switch to <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let toBranchName =\n            new Option<String>(\n                OptionName.ToBranchName,\n                [| \"-c\" |],\n                Required = false,\n                Description = \"The name of the branch to switch to.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let forceRecompute =\n            new Option<bool>(\n                OptionName.ForceRecompute,\n                Required = false,\n                Description = \"Force the re-computation of the recursive directory contents. [default: false]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let directoryVersionId =\n            new Option<DirectoryVersionId>(\n                OptionName.DirectoryVersionId,\n                [| \"-v\" |],\n                Required = false,\n                Description = \"The directory version ID to assign to the promotion <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n    //let listDirectories = new Option<bool>(\"--listDirectories\", Required = false, Description = \"Show directories when listing contents. [default: false]\")\n    //let listFiles = new Option<bool>(\"--listFiles\", Required = false, Description = \"Show files when listing contents. Implies --listDirectories. [default: false]\")\n\n    let mustBeAValidGuid (parseResult: ParseResult) (parameters: CommonParameters) (option: Option) (value: string) (error: BranchError) =\n        let mutable guid = Guid.Empty\n\n        if parseResult.GetResult(option) <> null\n           && not <| String.IsNullOrEmpty(value)\n           && (Guid.TryParse(value, &guid) = false\n               || guid = Guid.Empty) then\n            Error(GraceError.Create (getErrorMessage error) (getCorrelationId parseResult))\n        else\n            Ok(parseResult, parameters)\n\n    let mustBeAValidGraceName (parseResult: ParseResult) (parameters: CommonParameters) (option: Option) (value: string) (error: BranchError) =\n        if parseResult.GetResult(option) <> null\n           && not <| Constants.GraceNameRegex.IsMatch(value) then\n            Error(GraceError.Create (getErrorMessage error) (getCorrelationId parseResult))\n        else\n            Ok(parseResult, parameters)\n\n    let oneOfTheseOptionsMustBeProvided (parseResult: ParseResult) (options: Option array) (error: BranchError) =\n        match options\n              |> Array.tryFind (fun opt -> not <| isNull (parseResult.GetResult(opt)))\n            with\n        | Some opt -> Ok(parseResult)\n        | None -> Error(GraceError.Create (getErrorMessage error) (getCorrelationId parseResult))\n\n    /// Adjusts parameters to account for whether Id's or Name's were specified by the user, or should be taken from default values.\n    let normalizeIdsAndNames<'T when 'T :> CommonParameters> (parseResult: ParseResult) (parameters: 'T) =\n        if parseResult.GetResult(Options.ownerId).Implicit\n           && not\n              <| isNull (parseResult.GetResult(Options.ownerName))\n           && not\n              <| parseResult.GetResult(Options.ownerName).Implicit then\n            parameters.OwnerId <- String.Empty\n\n        if parseResult\n            .GetResult(\n                Options.organizationId\n            )\n               .Implicit\n           && not\n              <| isNull (parseResult.GetResult(Options.organizationName))\n           && not\n              <| parseResult\n                  .GetResult(\n                      Options.organizationName\n                  )\n                  .Implicit then\n            parameters.OrganizationId <- String.Empty\n\n        if parseResult\n            .GetResult(\n                Options.repositoryId\n            )\n               .Implicit\n           && not\n              <| isNull (parseResult.GetResult(Options.repositoryName))\n           && not\n              <| parseResult\n                  .GetResult(\n                      Options.repositoryName\n                  )\n                  .Implicit then\n            parameters.RepositoryId <- String.Empty\n\n        parameters\n\n    let private CommonValidations parseResult =\n        let ``Message must not be empty`` (parseResult: ParseResult) =\n            if parseResult.CommandResult.Command.Options.FirstOrDefault(fun option -> option.Name = OptionName.Message)\n               <> null then\n                let message =\n                    parseResult\n                        .GetValue<string>(OptionName.Message)\n                        .Trim()\n\n                if not <| String.IsNullOrEmpty(message) then\n                    Ok(parseResult)\n                else\n                    Error(GraceError.Create (getErrorMessage BranchError.MessageIsRequired) (getCorrelationId parseResult))\n            else\n                Ok(parseResult)\n\n        let ``Message must be less than 2048 characters`` (parseResult: ParseResult) =\n            if parseResult.CommandResult.Command.Options.FirstOrDefault(fun option -> option.Name = OptionName.Message)\n               <> null then\n                let message =\n                    parseResult\n                        .GetValue<string>(OptionName.Message)\n                        .Trim()\n\n                if message.Length <= 2048 then\n                    Ok(parseResult)\n                else\n                    Error(GraceError.Create (getErrorMessage BranchError.StringIsTooLong) (getCorrelationId parseResult))\n            else\n                Ok(parseResult)\n\n        (parseResult)\n        |> ``Message must not be empty``\n        >>= ``Message must be less than 2048 characters``\n\n    let private ``BranchName must not be empty`` (parseResult: ParseResult) =\n        let graceIds = getNormalizedIdsAndNames parseResult\n\n        if (parseResult.CommandResult.Command.Options.Contains(Options.branchNameRequired)\n            || parseResult.CommandResult.Command.Options.Contains(Options.branchName))\n           && not <| String.IsNullOrEmpty(graceIds.BranchName) then\n            Ok parseResult\n        else\n            Error(GraceError.Create (getErrorMessage BranchError.BranchNameIsRequired) (getCorrelationId parseResult))\n\n    let private valueOrEmpty (value: string) = if String.IsNullOrWhiteSpace(value) then String.Empty else value\n\n    let private guidToString (value: Guid) = if value = Guid.Empty then String.Empty else $\"{value}\"\n\n    let private fallbackString hasValue supplied fallbackValue = if hasValue then supplied |> valueOrEmpty else fallbackValue |> valueOrEmpty\n\n    let private fallbackGuidString hasValue supplied fallbackValue = if hasValue then supplied |> valueOrEmpty else fallbackValue |> guidToString\n\n    // Create subcommand.\n    type Create() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> CommonValidations\n                        >>= ``BranchName must not be empty``\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        // In a Create() command, if --branch-id is implicit, that's the current branch taken from graceconfig.json, and the\n                        //   current branch, by default, is the parent branch of the new one. Therefore, we need to set BranchId to a new Guid.\n                        let mutable graceIds = parseResult |> getNormalizedIdsAndNames\n\n                        if parseResult.GetResult(Options.branchId).Implicit then\n                            let branchId = Guid.NewGuid()\n                            graceIds <- { graceIds with BranchId = branchId; BranchIdString = $\"{branchId}\" }\n\n                        let parentBranchId = parseResult.GetValue(Options.parentBranchId)\n                        let parentBranchNameResult = parseResult.GetResult(Options.parentBranchName)\n                        let parentBranchIdResult = parseResult.GetResult(Options.parentBranchId)\n\n                        let parentBranchNameExplicit =\n                            not <| isNull parentBranchNameResult\n                            && not parentBranchNameResult.Implicit\n\n                        let parentBranchIdExplicit =\n                            not <| isNull parentBranchIdResult\n                            && not parentBranchIdResult.Implicit\n\n                        let parentBranchName =\n                            let suppliedParentBranchName =\n                                parseResult.GetValue(Options.parentBranchName)\n                                |> valueOrEmpty\n\n                            if not parentBranchNameExplicit\n                               && not parentBranchIdExplicit\n                               && suppliedParentBranchName = String.Empty\n                               && parentBranchId = Guid.Empty then\n                                Current().BranchName\n                            else\n                                suppliedParentBranchName\n\n                        let! parentBranchIdString =\n                            task {\n                                match parentBranchId, parentBranchName with\n                                | parentBranchId, parentBranchName when parentBranchId <> Guid.Empty -> return parentBranchId.ToString()\n                                | parentBranchId, parentBranchName when parentBranchName <> String.Empty -> return String.Empty\n                                | _ ->\n                                    // No parent specified, determine based on current branch's promotion support\n                                    if parseResult.GetResult(Options.branchId).Implicit then\n                                        // Get the current branch (before we changed graceIds.BranchId to the new branch)\n                                        let currentBranchId = Current().BranchId\n\n                                        if currentBranchId <> Guid.Empty then\n                                            // Get current branch details to check if it supports promotions\n                                            let currentBranchParameters =\n                                                GetBranchParameters(\n                                                    OwnerId = graceIds.OwnerIdString,\n                                                    OwnerName = graceIds.OwnerName,\n                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                    OrganizationName = graceIds.OrganizationName,\n                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                    RepositoryName = graceIds.RepositoryName,\n                                                    BranchId = $\"{currentBranchId}\",\n                                                    BranchName = String.Empty,\n                                                    CorrelationId = graceIds.CorrelationId\n                                                )\n\n                                            match! Branch.Get(currentBranchParameters) with\n                                            | Ok returnValue ->\n                                                let currentBranch = returnValue.ReturnValue\n                                                // If current branch supports promotions, use it as parent\n                                                if currentBranch.PromotionEnabled then\n                                                    return $\"{currentBranchId}\"\n                                                // If current branch doesn't support promotions, use its parent (if valid)\n                                                elif currentBranch.ParentBranchId <> Guid.Empty then\n                                                    return $\"{currentBranch.ParentBranchId}\"\n                                                else\n                                                    // Current branch has no valid parent, let server handle the error\n                                                    return String.Empty\n                                            | Error _ ->\n                                                // If we can't get current branch info, let server handle validation\n                                                return String.Empty\n                                        else\n                                            return String.Empty\n                                    else\n                                        return String.Empty\n                            }\n\n                        let initialPermissions =\n                            match parseResult.GetValue(Options.initialPermissions) with\n                            | null -> Array.empty<ReferenceType>\n                            | permissions -> permissions\n\n                        let parameters =\n                            CreateBranchParameters(\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                ParentBranchId = parentBranchIdString,\n                                ParentBranchName = parentBranchName,\n                                InitialPermissions = initialPermissions,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Branch.Create(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Branch.Create(parameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                let newConfig = Current()\n                                newConfig.BranchId <- Guid.Parse($\"{returnValue.Properties[nameof BranchId]}\")\n                                newConfig.BranchName <- $\"{returnValue.Properties[nameof BranchName]}\"\n                                updateConfiguration newConfig\n\n                            return result |> renderOutput parseResult\n                        | Error _ -> return result |> renderOutput parseResult\n                    | Error error ->\n                        return\n                            GraceResult.Error error\n                            |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    let private getRecursiveSizeImpl (parseResult: ParseResult) : Tasks.Task<int> =\n        if parseResult |> verbose then printParseResult parseResult\n\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n        let validateIncomingParameters = parseResult |> CommonValidations\n        let correlationId = getCorrelationId parseResult\n\n        match validateIncomingParameters with\n        | Error error ->\n            Task.FromResult(\n                GraceResult.Error error\n                |> renderOutput parseResult\n            )\n        | Ok _ ->\n            let referenceId =\n                if isNull (parseResult.GetResult(Options.referenceId)) then\n                    String.Empty\n                else\n                    parseResult\n                        .GetValue(Options.referenceId)\n                        .ToString()\n\n            let sha256Hash =\n                if isNull (parseResult.GetResult(Options.sha256Hash)) then\n                    String.Empty\n                else\n                    parseResult.GetValue(Options.sha256Hash)\n\n            let sdkParameters =\n                Parameters.Branch.ListContentsParameters(\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    Sha256Hash = sha256Hash,\n                    ReferenceId = referenceId,\n                    Pattern = String.Empty,\n                    ShowDirectories = true,\n                    ShowFiles = true,\n                    ForceRecompute = false,\n                    CorrelationId = correlationId\n                )\n\n            task {\n                let! result =\n                    if parseResult |> hasOutput then\n                        progress\n                            .Columns(progressColumns)\n                            .StartAsync(fun progressContext ->\n                                task {\n                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                    let! response = Branch.GetRecursiveSize(sdkParameters)\n                                    t0.Increment(100.0)\n                                    return response\n                                })\n                    else\n                        Branch.GetRecursiveSize(sdkParameters)\n\n                match result with\n                | Ok returnValue -> AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Total file size: {returnValue.ReturnValue:N0}[/]\"\n                | Error error -> AnsiConsole.MarkupLine $\"[{Colors.Error}]{error}[/]\"\n\n                return result |> renderOutput parseResult\n            }\n\n    type GetRecursiveSize() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    return! getRecursiveSizeImpl parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    let private getShortHash (sha256Hash: Sha256Hash) =\n        if String.IsNullOrWhiteSpace(sha256Hash) then String.Empty\n        elif sha256Hash.Length <= 8 then sha256Hash\n        else sha256Hash.Substring(0, 8)\n\n    let private tryGetRootSha256Hash (directoryVersions: IEnumerable<DirectoryVersion>) =\n        directoryVersions\n        |> Seq.tryFind (fun directoryVersion -> directoryVersion.RelativePath = Constants.RootDirectoryPath)\n        |> Option.map (fun directoryVersion -> directoryVersion.Sha256Hash)\n\n    let printContents (parseResult: ParseResult) (directoryVersions: IEnumerable<DirectoryVersion>) =\n        let directoryVersionArray = directoryVersions |> Seq.toArray\n\n        if directoryVersionArray.Length > 0 then\n            let longestRelativePath =\n                getLongestRelativePath (\n                    directoryVersionArray\n                    |> Seq.map (fun directoryVersion -> directoryVersion.ToLocalDirectoryVersion(DateTime.UtcNow))\n                )\n            //logToAnsiConsole Colors.Verbose $\"In printContents: getLongestRelativePath: {longestRelativePath}\"\n            let additionalSpaces = String.replicate (longestRelativePath - 2) \" \"\n            let additionalImportantDashes = String.replicate (longestRelativePath + 3) \"-\"\n            let additionalDeemphasizedDashes = String.replicate (38) \"-\"\n\n            directoryVersionArray\n            |> Seq.iteri (fun i directoryVersion ->\n                AnsiConsole.WriteLine()\n\n                if i = 0 then\n                    AnsiConsole.MarkupLine(\n                        $\"[{Colors.Important}]Created At                   SHA-256            Size  Path{additionalSpaces}[/][{Colors.Deemphasized}] (DirectoryVersionId)[/]\"\n                    )\n\n                    AnsiConsole.MarkupLine(\n                        $\"[{Colors.Important}]-----------------------------------------------------{additionalImportantDashes}[/][{Colors.Deemphasized}] {additionalDeemphasizedDashes}[/]\"\n                    )\n                //logToAnsiConsole Colors.Verbose $\"In printContents: directoryVersion.RelativePath: {directoryVersion.RelativePath}\"\n                let rightAlignedDirectoryVersionId =\n                    (String.replicate\n                        (longestRelativePath\n                         - directoryVersion.RelativePath.Length)\n                        \" \")\n                    + $\"({directoryVersion.DirectoryVersionId})\"\n\n                AnsiConsole.MarkupLine(\n                    $\"[{Colors.Highlighted}]{formatInstantAligned directoryVersion.CreatedAt}   {getShortSha256Hash directoryVersion.Sha256Hash}  {directoryVersion.Size, 13:N0}  /{directoryVersion.RelativePath}[/] [{Colors.Deemphasized}] {rightAlignedDirectoryVersionId}[/]\"\n                )\n                //if parseResult.CommandResult.Command.Options.Contains(Options.listFiles) then\n                let sortedFiles = directoryVersion.Files.OrderBy(fun f -> f.RelativePath)\n\n                for file in sortedFiles do\n                    AnsiConsole.MarkupLine(\n                        $\"[{Colors.Verbose}]{formatInstantAligned file.CreatedAt}   {getShortSha256Hash file.Sha256Hash}  {file.Size, 13:N0}  |- {file.RelativePath.Split('/').LastOrDefault()}[/]\"\n                    ))\n\n    let private listContentsImpl (parseResult: ParseResult) : Tasks.Task<int> =\n        if parseResult |> verbose then printParseResult parseResult\n\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n        let validateIncomingParameters = parseResult |> CommonValidations\n        let correlationId = getCorrelationId parseResult\n\n        match validateIncomingParameters with\n        | Error error ->\n            Task.FromResult(\n                GraceResult.Error error\n                |> renderOutput parseResult\n            )\n        | Ok _ ->\n            let referenceId =\n                if isNull (parseResult.GetResult Options.referenceId) then\n                    String.Empty\n                else\n                    (parseResult.GetValue Options.referenceId)\n                        .ToString()\n\n            let sha256Hash =\n                if isNull (parseResult.GetResult Options.sha256Hash) then\n                    String.Empty\n                else\n                    parseResult.GetValue Options.sha256Hash\n\n            let forceRecompute = parseResult.GetValue Options.forceRecompute\n\n            let sdkParameters =\n                ListContentsParameters(\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    Sha256Hash = sha256Hash,\n                    ReferenceId = referenceId,\n                    Pattern = String.Empty,\n                    ShowDirectories = true,\n                    ShowFiles = true,\n                    ForceRecompute = forceRecompute,\n                    CorrelationId = correlationId\n                )\n\n            task {\n                let! result =\n                    if parseResult |> hasOutput then\n                        progress\n                            .Columns(progressColumns)\n                            .StartAsync(fun progressContext ->\n                                task {\n                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                    let! response = Branch.ListContents(sdkParameters)\n                                    t0.Value <- 100.0\n                                    return response\n                                })\n                    else\n                        Branch.ListContents(sdkParameters)\n\n                match result with\n                | Ok returnValue ->\n                    let directoryVersions =\n                        returnValue\n                            .ReturnValue\n                            .Select(fun directoryVersionDto -> directoryVersionDto.DirectoryVersion)\n                            .OrderBy(fun dv -> dv.RelativePath)\n                            .ToArray()\n\n                    let directoryCount = directoryVersions.Length\n\n                    let fileCount = directoryVersions.Sum(fun directoryVersion -> directoryVersion.Files.Count)\n\n                    let totalFileSize = directoryVersions.Sum(fun directoryVersion -> directoryVersion.Files.Sum(fun f -> f.Size))\n\n                    AnsiConsole.MarkupLine($\"[{Colors.Important}]All values taken from the selected version of this branch from the server.[/]\")\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of directories: {directoryCount}.[/]\")\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of files: {fileCount}; total file size: {totalFileSize:N0}.[/]\")\n\n                    match tryGetRootSha256Hash directoryVersions with\n                    | Some rootSha256Hash -> AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Root SHA-256 hash: {getShortHash rootSha256Hash}[/]\")\n                    | None -> AnsiConsole.MarkupLine($\"[{Colors.Error}]Root SHA-256 hash: unavailable (root directory entry missing from server response).[/]\")\n\n                    if directoryCount > 0 then\n                        printContents parseResult directoryVersions\n                    else\n                        AnsiConsole.MarkupLine($\"[{Colors.Verbose}]No directory entries were returned.[/]\")\n                | Error _ -> ()\n\n                return result |> renderOutput parseResult\n            }\n\n    type ListContents() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) =\n            task {\n                try\n                    return! listContentsImpl parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type SetName() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n                    let correlationId = getCorrelationId parseResult\n                    let newName = parseResult.GetValue(Options.newName)\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Branch.SetBranchNameParameters(\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                NewName = newName,\n                                CorrelationId = correlationId\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Branch.SetName(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Branch.SetName(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error ->\n                        return\n                            GraceResult.Error error\n                            |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    let private assignImpl (parseResult: ParseResult) : Tasks.Task<int> =\n        if parseResult |> verbose then printParseResult parseResult\n\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n        let correlationId = getCorrelationId parseResult\n        let validateIncomingParameters = parseResult |> CommonValidations\n\n        let requiredInputs =\n            oneOfTheseOptionsMustBeProvided\n                parseResult\n                [|\n                    Options.directoryVersionId\n                    Options.sha256Hash\n                |]\n                BranchError.EitherDirectoryVersionIdOrSha256HashRequired\n\n        match validateIncomingParameters, requiredInputs with\n        | Error error, _\n        | _, Error error ->\n            Task.FromResult(\n                GraceResult.Error error\n                |> renderOutput parseResult\n            )\n        | Ok _, Ok _ ->\n            let directoryVersionId =\n                if isNull (parseResult.GetResult(Options.directoryVersionId)) then\n                    Guid.Empty\n                else\n                    parseResult.GetValue(Options.directoryVersionId)\n\n            let sha256Hash =\n                if isNull (parseResult.GetResult(Options.sha256Hash)) then\n                    String.Empty\n                else\n                    parseResult.GetValue(Options.sha256Hash)\n\n            let parameters =\n                Parameters.Branch.AssignParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    DirectoryVersionId = directoryVersionId,\n                    Sha256Hash = sha256Hash,\n                    CorrelationId = correlationId\n                )\n\n            task {\n                let! result =\n                    if parseResult |> hasOutput then\n                        progress\n                            .Columns(progressColumns)\n                            .StartAsync(fun progressContext ->\n                                task {\n                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                    let! response = Branch.Assign(parameters)\n                                    t0.Increment(100.0)\n                                    return response\n                                })\n                    else\n                        Branch.Assign(parameters)\n\n                return result |> renderOutput parseResult\n            }\n\n    type Assign() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    return! assignImpl parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    let private validateAndCleanMessage (message: string) (correlationId: CorrelationId) : Result<string, GraceError> =\n\n        // Helpers local to this function to keep things simple.\n        let fail msg = Error(GraceError.Create msg correlationId)\n\n        // Regexes: created per-call for simplicity;\n        // you can hoist them to module-level if you want them compiled once.\n        let disallowedControlChars = Regex(\"[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]\", RegexOptions.Compiled)\n        let bidiControls = Regex(\"[\\u202A-\\u202E\\u2066-\\u2069]\", RegexOptions.Compiled)\n        let nonWhitespace = Regex(@\"\\S\", RegexOptions.Compiled)\n\n        // 1. Normalize newlines to '\\n'\n        let step1 =\n            message\n                .Trim()\n                .Replace(\"\\r\\n\", \"\\n\")\n                .Replace(\"\\r\", \"\\n\")\n\n        // 2. Trim trailing whitespace per line\n        let step2 =\n            let lines = step1.Split('\\n')\n            let trimmedLines = lines |> Array.map (fun line -> line.TrimEnd())\n            String.concat \"\\n\" trimmedLines\n\n        // 3. Remove leading / trailing *blank* lines (whitespace-only)\n        let step3 =\n            let lines = step2.Split('\\n')\n\n            let mutable start = 0\n            let mutable finish = lines.Length - 1\n\n            while start <= finish\n                  && String.IsNullOrWhiteSpace lines[start] do\n                start <- start + 1\n\n            while finish >= start\n                  && String.IsNullOrWhiteSpace lines[finish] do\n                finish <- finish - 1\n\n            if start > finish then \"\" else String.concat \"\\n\" lines[start..finish]\n\n        // 4. Unicode normalization to NFC\n        let cleaned = step3.Normalize(NormalizationForm.FormC)\n\n        // 5. Actual validations\n\n        // 5a. Must contain at least one non-whitespace character\n        if\n            String.IsNullOrEmpty cleaned\n            || not (nonWhitespace.IsMatch cleaned)\n        then\n            fail \"Message must contain at least one non-whitespace character.\"\n        // 5b. Disallow unwanted control characters\n        elif disallowedControlChars.IsMatch cleaned then\n            fail \"Message contains disallowed control characters.\"\n        // 5c. Disallow bidi control characters\n        elif bidiControls.IsMatch cleaned then\n            fail \"Message contains disallowed Unicode bidi control characters.\"\n        else\n            Ok cleaned\n\n    type CreateReferenceCommand = CreateReferenceParameters -> Task<GraceResult<String>>\n\n    let private createReferenceHandler (parseResult: ParseResult) (message: string) (command: CreateReferenceCommand) (commandType: string) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> CommonValidations\n                let referenceMessage = validateAndCleanMessage message (getCorrelationId parseResult)\n\n                match (validateIncomingParameters, referenceMessage) with\n                | Ok _, Ok referenceMessage ->\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    //let sha256Bytes = SHA256.HashData(Encoding.ASCII.GetBytes(rnd.NextInt64().ToString(\"x8\")))\n                    //let sha256Hash = Seq.fold (fun (sb: StringBuilder) currentByte ->\n                    //    sb.Append(sprintf $\"{currentByte:X2}\")) (StringBuilder(sha256Bytes.Length)) sha256Bytes\n\n                    if parseResult |> hasOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace status file.[/]\")\n\n                                        let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Scanning working directory for changes.[/]\", autoStart = false)\n\n                                        let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating new directory verions.[/]\", autoStart = false)\n\n                                        let t3 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading changed files to object storage.[/]\", autoStart = false)\n\n                                        let t4 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading new directory versions.[/]\", autoStart = false)\n\n                                        let t5 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating new {commandType}.[/]\", autoStart = false)\n\n                                        //let mutable rootDirectoryId = DirectoryId.Empty\n                                        //let mutable rootDirectorySha256Hash = Sha256Hash String.Empty\n                                        let rootDirectoryVersion = ref (DirectoryVersionId.Empty, Sha256Hash String.Empty)\n\n                                        match! getGraceWatchStatus () with\n                                        | Some graceWatchStatus ->\n                                            t0.Value <- 100.0\n                                            t1.Value <- 100.0\n                                            t2.Value <- 100.0\n                                            t3.Value <- 100.0\n                                            t4.Value <- 100.0\n\n                                            rootDirectoryVersion.Value <- (graceWatchStatus.RootDirectoryId, graceWatchStatus.RootDirectorySha256Hash)\n                                        | None ->\n                                            t0.StartTask() // Read Grace status file.\n                                            let! previousGraceStatus = readGraceStatusFile ()\n                                            let mutable newGraceStatus = previousGraceStatus\n                                            t0.Value <- 100.0\n\n                                            t1.StartTask() // Scan for differences.\n                                            let! differences = scanForDifferences previousGraceStatus\n                                            //logToAnsiConsole Colors.Verbose $\"differences: {serialize differences}\"\n                                            let! newFileVersions = copyUpdatedFilesToObjectCache t1 differences\n                                            //logToAnsiConsole Colors.Verbose $\"newFileVersions: {serialize newFileVersions}\"\n                                            t1.Value <- 100.0\n\n                                            t2.StartTask() // Create new directory versions.\n\n                                            let! (updatedGraceStatus, newDirectoryVersions) =\n                                                getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                                            newGraceStatus <- updatedGraceStatus\n\n                                            rootDirectoryVersion.Value <- (newGraceStatus.RootDirectoryId, newGraceStatus.RootDirectorySha256Hash)\n\n                                            t2.Value <- 100.0\n\n                                            t3.StartTask() // Upload to object storage.\n\n                                            let updatedRelativePaths =\n                                                differences\n                                                    .Select(fun difference ->\n                                                        match difference.DifferenceType with\n                                                        | Add ->\n                                                            match difference.FileSystemEntryType with\n                                                            | FileSystemEntryType.File -> Some difference.RelativePath\n                                                            | FileSystemEntryType.Directory -> None\n                                                        | Change ->\n                                                            match difference.FileSystemEntryType with\n                                                            | FileSystemEntryType.File -> Some difference.RelativePath\n                                                            | FileSystemEntryType.Directory -> None\n                                                        | Delete -> None)\n                                                    .Where(fun relativePathOption -> relativePathOption.IsSome)\n                                                    .Select(fun relativePath -> relativePath.Value)\n\n                                            // let newFileVersions = updatedRelativePaths.Select(fun relativePath ->\n                                            //     newDirectoryVersions.First(fun dv -> dv.Files.Exists(fun file -> file.RelativePath = relativePath)).Files.First(fun file -> file.RelativePath = relativePath))\n\n                                            let mutable lastFileUploadInstant = newGraceStatus.LastSuccessfulFileUpload\n\n                                            if newFileVersions.Count() > 0 then\n                                                let getUploadMetadataForFilesParameters =\n                                                    Storage.GetUploadMetadataForFilesParameters(\n                                                        OwnerId = graceIds.OwnerIdString,\n                                                        OwnerName = graceIds.OwnerName,\n                                                        OrganizationId = graceIds.OrganizationIdString,\n                                                        OrganizationName = graceIds.OrganizationName,\n                                                        RepositoryId = graceIds.RepositoryIdString,\n                                                        RepositoryName = graceIds.RepositoryName,\n                                                        CorrelationId = getCorrelationId parseResult,\n                                                        FileVersions =\n                                                            (newFileVersions\n                                                             |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                                             |> Seq.toArray)\n                                                    )\n\n                                                match! uploadFilesToObjectStorage getUploadMetadataForFilesParameters with\n                                                | Ok returnValue -> () //logToAnsiConsole Colors.Verbose $\"Uploaded all files to object storage.\"\n                                                | Error error -> logToAnsiConsole Colors.Error $\"Error uploading files to object storage: {error.Error}\"\n\n                                                lastFileUploadInstant <- getCurrentInstant ()\n\n                                            t3.Value <- 100.0\n\n                                            t4.StartTask() // Upload directory versions.\n\n                                            let mutable lastDirectoryVersionUpload = newGraceStatus.LastSuccessfulDirectoryVersionUpload\n\n                                            if newDirectoryVersions.Count > 0 then\n                                                let saveParameters = SaveDirectoryVersionsParameters()\n                                                saveParameters.OwnerId <- graceIds.OwnerIdString\n                                                saveParameters.OwnerName <- graceIds.OwnerName\n                                                saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                                                saveParameters.OrganizationName <- graceIds.OrganizationName\n                                                saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                                                saveParameters.RepositoryName <- graceIds.RepositoryName\n                                                saveParameters.DirectoryVersionId <- $\"{newGraceStatus.RootDirectoryId}\"\n\n                                                saveParameters.DirectoryVersions <-\n                                                    newDirectoryVersions\n                                                        .Select(fun dv -> dv.ToDirectoryVersion)\n                                                        .ToList()\n\n                                                saveParameters.CorrelationId <- getCorrelationId parseResult\n\n                                                let! uploadDirectoryVersions = DirectoryVersion.SaveDirectoryVersions saveParameters\n\n                                                lastDirectoryVersionUpload <- getCurrentInstant ()\n\n                                            t4.Value <- 100.0\n\n                                            newGraceStatus <-\n                                                { newGraceStatus with\n                                                    LastSuccessfulFileUpload = lastFileUploadInstant\n                                                    LastSuccessfulDirectoryVersionUpload = lastDirectoryVersionUpload\n                                                }\n\n                                            do! applyGraceStatusIncremental newGraceStatus newDirectoryVersions differences\n\n                                        t5.StartTask() // Create new reference.\n\n                                        let (rootDirectoryId, rootDirectorySha256Hash) = rootDirectoryVersion.Value\n\n                                        let sdkParameters =\n                                            Parameters.Branch.CreateReferenceParameters(\n                                                BranchId = graceIds.BranchIdString,\n                                                BranchName = graceIds.BranchName,\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                DirectoryVersionId = rootDirectoryId,\n                                                Sha256Hash = rootDirectorySha256Hash,\n                                                Message = referenceMessage,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        let! result = command sdkParameters\n                                        t5.Value <- 100.0\n\n                                        return result\n                                    })\n                    else\n                        let! previousGraceStatus = readGraceStatusFile ()\n                        let! differences = scanForDifferences previousGraceStatus\n\n                        let! (newGraceIndex, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                        let updatedRelativePaths =\n                            differences\n                                .Select(fun difference ->\n                                    match difference.DifferenceType with\n                                    | Add ->\n                                        match difference.FileSystemEntryType with\n                                        | FileSystemEntryType.File -> Some difference.RelativePath\n                                        | FileSystemEntryType.Directory -> None\n                                    | Change ->\n                                        match difference.FileSystemEntryType with\n                                        | FileSystemEntryType.File -> Some difference.RelativePath\n                                        | FileSystemEntryType.Directory -> None\n                                    | Delete -> None)\n                                .Where(fun relativePathOption -> relativePathOption.IsSome)\n                                .Select(fun relativePath -> relativePath.Value)\n\n                        let newFileVersions =\n                            updatedRelativePaths.Select (fun relativePath ->\n                                newDirectoryVersions\n                                    .First(fun dv -> dv.Files.Exists(fun file -> file.RelativePath = relativePath))\n                                    .Files.First(fun file -> file.RelativePath = relativePath))\n\n                        let getUploadMetadataForFilesParameters =\n                            Storage.GetUploadMetadataForFilesParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                FileVersions =\n                                    (newFileVersions\n                                     |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                     |> Seq.toArray)\n                            )\n\n                        let! uploadResult = uploadFilesToObjectStorage getUploadMetadataForFilesParameters\n                        let saveParameters = SaveDirectoryVersionsParameters()\n                        saveParameters.OwnerId <- graceIds.OwnerIdString\n                        saveParameters.OwnerName <- graceIds.OwnerName\n                        saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                        saveParameters.OrganizationName <- graceIds.OrganizationName\n                        saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                        saveParameters.RepositoryName <- graceIds.RepositoryName\n                        saveParameters.CorrelationId <- getCorrelationId parseResult\n\n                        saveParameters.DirectoryVersions <-\n                            newDirectoryVersions\n                                .Select(fun dv -> dv.ToDirectoryVersion)\n                                .ToList()\n\n                        let! uploadDirectoryVersions = DirectoryVersion.SaveDirectoryVersions saveParameters\n                        let rootDirectoryVersion = getRootDirectoryVersion previousGraceStatus\n\n                        let sdkParameters =\n                            Parameters.Branch.CreateReferenceParameters(\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                DirectoryVersionId = rootDirectoryVersion.DirectoryVersionId,\n                                Sha256Hash = rootDirectoryVersion.Sha256Hash,\n                                Message = referenceMessage,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result = command sdkParameters\n                        return result\n                | Error error, _ -> return Error error\n                | _, Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    let private promotionHandler (parseResult: ParseResult) (message: string) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> CommonValidations\n                let sanitizedMessage = message.Trim()\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    if parseResult |> hasOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace status file.[/]\")\n\n                                        let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Checking if the promotion is valid.[/]\", autoStart = false)\n\n                                        let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\", autoStart = false)\n\n                                        // Read Grace status file.\n                                        let! graceStatus = readGraceStatusFile ()\n                                        let rootDirectoryId = graceStatus.RootDirectoryId\n                                        let rootDirectorySha256Hash = graceStatus.RootDirectorySha256Hash\n                                        t0.Value <- 100.0\n\n                                        // Check if the promotion is valid; i.e. it's allowed by the ReferenceTypes enabled in the repository.\n                                        t1.StartTask()\n                                        // For single-step promotion, the current branch's latest commit will become the parent branch's next promotion.\n                                        // If our current state is not the latest commit, print a warning message.\n\n                                        // Get the Dto for the current branch. That will have its latest commit.\n                                        let branchGetParameters =\n                                            GetBranchParameters(\n                                                BranchId = graceIds.BranchIdString,\n                                                BranchName = graceIds.BranchName,\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        //logToAnsiConsole\n                                        //    Colors.Verbose\n                                        //    $\"In promotionHandler: branchGetParameters:{Environment.NewLine}{serialize branchGetParameters}\"\n\n                                        let! branchResult = Branch.Get(branchGetParameters)\n\n                                        match branchResult with\n                                        | Ok branchReturnValue ->\n                                            // If we succeeded, get the parent branch Dto. That will have its latest promotion.\n                                            let! parentBranchResult = Branch.GetParentBranch(branchGetParameters)\n\n                                            match parentBranchResult with\n                                            | Ok parentBranchReturnValue ->\n                                                // Yay, we have both Dto's.\n                                                let branchDto = branchReturnValue.ReturnValue\n\n                                                //logToAnsiConsole Colors.Verbose $\"In promotionHandler: branchDto:{Environment.NewLine}{serialize branchDto}\"\n\n                                                let parentBranchDto = parentBranchReturnValue.ReturnValue\n\n                                                let referenceIds = List<ReferenceId>()\n\n                                                if branchDto.LatestCommit <> ReferenceDto.Default then\n                                                    referenceIds.Add(branchDto.LatestCommit.ReferenceId)\n\n                                                if branchDto.LatestPromotion <> ReferenceDto.Default then\n                                                    referenceIds.Add(branchDto.LatestPromotion.ReferenceId)\n\n                                                if referenceIds.Count > 0 then\n                                                    let getReferencesByReferenceIdParameters =\n                                                        Parameters.Repository.GetReferencesByReferenceIdParameters(\n                                                            OwnerId = graceIds.OwnerIdString,\n                                                            OwnerName = graceIds.OwnerName,\n                                                            OrganizationId = graceIds.OrganizationIdString,\n                                                            OrganizationName = graceIds.OrganizationName,\n                                                            RepositoryId = graceIds.RepositoryIdString,\n                                                            RepositoryName = graceIds.RepositoryName,\n                                                            ReferenceIds = referenceIds,\n                                                            CorrelationId = graceIds.CorrelationId\n                                                        )\n\n                                                    //logToAnsiConsole\n                                                    //    Colors.Verbose\n                                                    //    $\"In promotionHandler: getReferencesByReferenceIdParameters:{Environment.NewLine}{serialize getReferencesByReferenceIdParameters}\"\n\n                                                    match! Repository.GetReferencesByReferenceId(getReferencesByReferenceIdParameters) with\n                                                    | Ok returnValue ->\n                                                        let references = returnValue.ReturnValue\n\n                                                        let latestPromotableReference =\n                                                            references\n                                                                .OrderByDescending(fun reference -> reference.CreatedAt)\n                                                                .First()\n                                                        // If the current branch's latest reference is not the latest commit - i.e. they've done more work in the branch\n                                                        //   after the commit they're expecting to promote - print a warning.\n                                                        //match getReferencesByReferenceIdResult with\n                                                        //| Ok returnValue ->\n                                                        //    let references = returnValue.ReturnValue\n                                                        //    if referenceDto.DirectoryId <> graceStatus.RootDirectoryId then\n                                                        //        logToAnsiConsole Colors.Important $\"Note: the branch has been updated since the latest commit.\"\n                                                        //| Error error -> () // I don't really care if this call fails, it's just a warning message.\n                                                        t1.Value <- 100.0\n\n                                                        // If the current branch is based on the parent's latest promotion, then we can proceed with the promotion.\n                                                        if branchDto.BasedOn.ReferenceId = parentBranchDto.LatestPromotion.ReferenceId then\n                                                            t2.StartTask()\n\n                                                            let promotionParameters =\n                                                                Parameters.Branch.CreateReferenceParameters(\n                                                                    BranchId = $\"{parentBranchDto.BranchId}\",\n                                                                    OwnerId = graceIds.OwnerIdString,\n                                                                    OwnerName = graceIds.OwnerName,\n                                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                                    OrganizationName = graceIds.OrganizationName,\n                                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                                    RepositoryName = graceIds.RepositoryName,\n                                                                    DirectoryVersionId = latestPromotableReference.DirectoryId,\n                                                                    Sha256Hash = latestPromotableReference.Sha256Hash,\n                                                                    Message = sanitizedMessage,\n                                                                    CorrelationId = graceIds.CorrelationId\n                                                                )\n\n                                                            let! promotionResult = Branch.Promote(promotionParameters)\n\n                                                            match promotionResult with\n                                                            | Ok returnValue ->\n                                                                //logToAnsiConsole Colors.Verbose $\"Succeeded doing promotion.\"\n\n                                                                //logToAnsiConsole\n                                                                //    Colors.Verbose\n                                                                //    $\"{serialize (returnValue.Properties.OrderBy(fun kvp -> kvp.Key))}\"\n\n                                                                let promotionReferenceId = returnValue.Properties[ \"ReferenceId\" ].ToString()\n                                                                //let promotionReferenceId = returnValue.Properties.Item(nameof ReferenceId) :?> string\n\n                                                                let rebaseParameters =\n                                                                    Parameters.Branch.RebaseParameters(\n                                                                        BranchId = $\"{branchDto.BranchId}\",\n                                                                        RepositoryId = $\"{branchDto.RepositoryId}\",\n                                                                        OwnerId = graceIds.OwnerIdString,\n                                                                        OwnerName = graceIds.OwnerName,\n                                                                        OrganizationId = graceIds.OrganizationIdString,\n                                                                        OrganizationName = graceIds.OrganizationName,\n                                                                        BasedOn = Guid.Parse(promotionReferenceId)\n                                                                    )\n\n                                                                let! rebaseResult = Branch.Rebase(rebaseParameters)\n                                                                t2.Value <- 100.0\n\n                                                                match rebaseResult with\n                                                                | Ok returnValue ->\n                                                                    //logToAnsiConsole Colors.Verbose $\"Succeeded doing rebase.\"\n\n                                                                    return promotionResult\n                                                                | Error error -> return Error error\n                                                            | Error error ->\n                                                                t2.Value <- 100.0\n                                                                return Error error\n\n                                                        else\n                                                            return\n                                                                Error(\n                                                                    GraceError.Create\n                                                                        (getErrorMessage BranchError.BranchIsNotBasedOnLatestPromotion)\n                                                                        (parseResult |> getCorrelationId)\n                                                                )\n                                                    | Error error ->\n                                                        t2.Value <- 100.0\n                                                        return Error error\n                                                else\n                                                    return\n                                                        Error(\n                                                            GraceError.Create\n                                                                (getErrorMessage BranchError.PromotionNotAvailableBecauseThereAreNoPromotableReferences)\n                                                                (parseResult |> getCorrelationId)\n                                                        )\n                                            | Error error ->\n                                                t1.Value <- 100.0\n                                                return Error error\n                                        | Error error ->\n                                            t1.Value <- 100.0\n                                            return Error error\n                                    })\n                    else\n                        // Same result, with no output.\n                        return Error(GraceError.Create \"Need to implement the else clause.\" (parseResult |> getCorrelationId))\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    type Promote() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let message =\n                        parseResult.GetValue(Options.message)\n                        |> valueOrEmpty\n\n                    let! result = promotionHandler parseResult message\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type Commit() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let message =\n                        parseResult.GetValue(Options.messageRequired)\n                        |> valueOrEmpty\n\n                    let command (parameters: CreateReferenceParameters) = task { return! Branch.Commit(parameters) }\n\n                    let! result = createReferenceHandler parseResult message command (nameof(Commit).ToLowerInvariant())\n\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type Checkpoint() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let message =\n                        parseResult.GetValue(Options.message)\n                        |> valueOrEmpty\n\n                    let command (parameters: CreateReferenceParameters) = task { return! Branch.Checkpoint(parameters) }\n\n                    let! result = createReferenceHandler parseResult message command (nameof(Checkpoint).ToLowerInvariant())\n\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type Save() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let message =\n                        parseResult.GetValue(Options.message)\n                        |> valueOrEmpty\n\n                    let command (parameters: CreateReferenceParameters) = task { return! Branch.Save(parameters) }\n\n                    let! result = createReferenceHandler parseResult message command (nameof(Save).ToLowerInvariant())\n\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type Tag() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let message =\n                        parseResult.GetValue(Options.messageRequired)\n                        |> valueOrEmpty\n\n                    let command (parameters: CreateReferenceParameters) = task { return! Branch.Tag(parameters) }\n\n                    let! result = createReferenceHandler parseResult message command (nameof(Tag).ToLowerInvariant())\n\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type CreateExternal() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let message =\n                        parseResult.GetValue(Options.messageRequired)\n                        |> valueOrEmpty\n\n                    let command (parameters: CreateReferenceParameters) = task { return! Branch.CreateExternal(parameters) }\n\n                    let! result = createReferenceHandler parseResult message command (\"External\".ToLowerInvariant())\n\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableFeatureCommand = EnableFeatureParameters -> Task<GraceResult<string>>\n\n    let private enableFeatureHandler (parseResult: ParseResult) (enabled: bool) (command: EnableFeatureCommand) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> CommonValidations\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let parameters =\n                        Parameters.Branch.EnableFeatureParameters(\n                            BranchId = graceIds.BranchIdString,\n                            BranchName = graceIds.BranchName,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            Enabled = enabled,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = command parameters\n\n                    match result with\n                    | Ok returnValue -> return Ok(GraceReturnValue.Create (returnValue.ReturnValue) graceIds.CorrelationId)\n                    | Error error -> return Error error\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    type EnableAssign() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableAssign(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnablePromotion() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnablePromotion(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableCommit() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableCommit(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableCheckpoint() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableCheckpoint(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableSave() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableSave(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableTag() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableTag(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableExternal() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableExternal(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type EnableAutoRebase() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let enabled = parseResult.GetValue(Options.enabled)\n                    let command (parameters: EnableFeatureParameters) = task { return! Branch.EnableAutoRebase(parameters) }\n\n                    let! result = enableFeatureHandler parseResult enabled command\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type SetPromotionMode() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let graceIds = parseResult |> getNormalizedIdsAndNames\n                        let promotionMode = parseResult.GetValue(Options.promotionMode)\n\n                        let parameters =\n                            Parameters.Branch.SetPromotionModeParameters(\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                PromotionMode = promotionMode,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Branch.SetPromotionMode(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Branch.SetPromotionMode(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    // Get subcommand\n    type Get() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let includeDeleted = parseResult.GetValue(Options.includeDeleted)\n                    let showEvents = parseResult.GetValue(Options.showEvents)\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                        let branchParameters =\n                            GetBranchParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                IncludeDeleted = includeDeleted,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Branch.Get(branchParameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Branch.Get(branchParameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                            AnsiConsole.Write(jsonText)\n                            AnsiConsole.WriteLine()\n\n                            if showEvents then\n                                let eventsParameters =\n                                    GetBranchVersionParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = graceIds.RepositoryIdString,\n                                        RepositoryName = graceIds.RepositoryName,\n                                        BranchId = graceIds.BranchIdString,\n                                        BranchName = graceIds.BranchName,\n                                        IncludeDeleted = includeDeleted,\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                let! eventsResult =\n                                    if parseResult |> hasOutput then\n                                        progress\n                                            .Columns(progressColumns)\n                                            .StartAsync(fun progressContext ->\n                                                task {\n                                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                                    let! response = Branch.GetEvents(eventsParameters)\n                                                    t0.Increment(100.0)\n                                                    return response\n                                                })\n                                    else\n                                        Branch.GetEvents(eventsParameters)\n\n                                match eventsResult with\n                                | Ok eventsValue ->\n                                    let sb = StringBuilder()\n\n                                    for line in eventsValue.ReturnValue do\n                                        sb.AppendLine($\"{Markup.Escape(line)},\") |> ignore\n                                        AnsiConsole.MarkupLine $\"[{Colors.Verbose}]{Markup.Escape(line)}[/]\"\n\n                                    if sb.Length > 0 then sb.Remove(sb.Length - 1, 1) |> ignore\n\n                                    AnsiConsole.WriteLine()\n                                    return 0\n                                | Error graceError -> return renderOutput parseResult (GraceResult.Error graceError)\n                            else\n                                return 0\n                        | Error graceError -> return renderOutput parseResult (GraceResult.Error graceError)\n                    | Error graceError -> return renderOutput parseResult (GraceResult.Error graceError)\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n\n    type GetReferenceQuery = GetReferencesParameters -> Task<GraceResult<ReferenceDto array>>\n\n    let private fetchReferences\n        (getBranchParameters: GetBranchParameters)\n        (getReferencesParameters: GetReferencesParameters)\n        (query: GetReferenceQuery)\n        (graceIds: GraceIds)\n        =\n        task {\n            let! branchResult = Branch.Get(getBranchParameters)\n            let! referencesResult = query getReferencesParameters\n\n            match (branchResult, referencesResult) with\n            | (Ok branchValue, Ok referencesValue) ->\n                let graceReturnValue = GraceReturnValue.Create (branchValue.ReturnValue, referencesValue.ReturnValue) graceIds.CorrelationId\n\n                referencesValue.Properties\n                |> Seq.iter (fun kvp -> graceReturnValue.Properties.Add(kvp.Key, kvp.Value))\n\n                return Ok graceReturnValue\n            | (_, Error error)\n            | (Error error, _) -> return Error error\n        }\n\n    let private getReferenceHandlerImpl (parseResult: ParseResult) (maxCount: int) (query: GetReferenceQuery) =\n        if parseResult |> verbose then printParseResult parseResult\n\n        let validateIncomingParameters = parseResult |> CommonValidations\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n        match validateIncomingParameters with\n        | Error error -> Task.FromResult(Error error)\n        | Ok _ ->\n            let getBranchParameters =\n                GetBranchParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            let getReferencesParameters =\n                GetReferencesParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    MaxCount = maxCount,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            let fetch () = fetchReferences getBranchParameters getReferencesParameters query graceIds\n\n            if parseResult |> hasOutput then\n                progress\n                    .Columns(progressColumns)\n                    .StartAsync(fun progressContext ->\n                        task {\n                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                            let! response = fetch ()\n                            t0.Increment(100.0)\n                            return response\n                        })\n            else\n                fetch ()\n\n    let getReferenceHandler (parseResult: ParseResult) (maxCount: int) (query: GetReferenceQuery) =\n        task {\n            try\n                return! getReferenceHandlerImpl parseResult maxCount query\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    let private createReferenceTable (parseResult: ParseResult) (references: ReferenceDto array) =\n        let sortedResults =\n            references\n            |> Array.sortByDescending (fun row -> row.CreatedAt)\n\n        let table = Table(Border = TableBorder.Rounded, ShowHeaders = true)\n\n        table.AddColumns(\n            [|\n                TableColumn($\"[{Colors.Important}]Type[/]\")\n                TableColumn($\"[{Colors.Important}]Message[/]\")\n                TableColumn($\"[{Colors.Important}]SHA-256[/]\")\n                TableColumn($\"[{Colors.Important}]When[/]\", Alignment = Justify.Right)\n                TableColumn($\"[{Colors.Important}][/]\")\n            |]\n        )\n        |> ignore\n\n        if parseResult |> verbose then\n            table\n                .AddColumns(\n                    [|\n                        TableColumn($\"[{Colors.Deemphasized}]ReferenceId[/]\")\n                    |]\n                )\n                .AddColumns(\n                    [|\n                        TableColumn($\"[{Colors.Deemphasized}]Root DirectoryVersionId[/]\")\n                    |]\n                )\n            |> ignore\n\n        for row in sortedResults do\n            //logToAnsiConsole Colors.Verbose $\"{serialize row}\"\n            let sha256Hash =\n                if parseResult.GetValue(Options.fullSha) then\n                    $\"{row.Sha256Hash}\"\n                else\n                    $\"{getShortSha256Hash row.Sha256Hash}\"\n\n            let localCreatedAtTime = row.CreatedAt.ToDateTimeUtc().ToLocalTime()\n\n            let referenceTime = $\"\"\"{localCreatedAtTime.ToString(\"g\", CultureInfo.CurrentUICulture)}\"\"\"\n\n            if parseResult |> verbose then\n                table.AddRow(\n                    [|\n                        $\"{getDiscriminatedUnionCaseName (row.ReferenceType)}\"\n                        $\"{row.ReferenceText}\"\n                        sha256Hash\n                        ago row.CreatedAt\n                        $\"[{Colors.Deemphasized}]{referenceTime}[/]\"\n                        $\"[{Colors.Deemphasized}]{row.ReferenceId}[/]\"\n                        $\"[{Colors.Deemphasized}]{row.DirectoryId}[/]\"\n                    |]\n                )\n            else\n                table.AddRow(\n                    [|\n                        $\"{getDiscriminatedUnionCaseName (row.ReferenceType)}\"\n                        $\"{row.ReferenceText}\"\n                        sha256Hash\n                        ago row.CreatedAt\n                        $\"[{Colors.Deemphasized}]{referenceTime}[/]\"\n                    |]\n                )\n            |> ignore\n\n        table\n\n    let printReferenceTable (table: Table) (references: ReferenceDto array) branchName referenceName =\n        AnsiConsole.MarkupLine($\"[{Colors.Important}]{referenceName} in branch {branchName}:[/]\")\n        AnsiConsole.Write(table)\n        AnsiConsole.MarkupLine($\"[{Colors.Important}]Returned {references.Length} rows.[/]\")\n\n    let private renderReferencesOutput (parseResult: ParseResult) (label: string) (result: GraceResult<BranchDto * ReferenceDto array>) =\n        match result with\n        | Ok graceReturnValue ->\n            let (branchDto, references) = graceReturnValue.ReturnValue\n            let rendered = result |> renderOutput parseResult\n\n            if parseResult |> hasOutput then\n                let referenceTable = createReferenceTable parseResult references\n                printReferenceTable referenceTable references branchDto.BranchName label\n\n            rendered\n        | Error _ -> result |> renderOutput parseResult\n\n    type GetReferences() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n                    let query (parameters: GetReferencesParameters) = Branch.GetReferences parameters\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"References\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type GetPromotions() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n                    let query (parameters: GetReferencesParameters) = task { return! Branch.GetPromotions(parameters) }\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"Promotions\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type GetCommits() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n                    let query (parameters: GetReferencesParameters) = task { return! Branch.GetCommits(parameters) }\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"Commits\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type GetCheckpoints() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n\n                    let query (parameters: GetReferencesParameters) =\n                        task {\n                            let! checkpointsResult = Branch.GetCheckpoints(parameters)\n\n                            match checkpointsResult with\n                            | Ok checkpointsValue ->\n                                let! commitsResult = Branch.GetCommits(parameters)\n\n                                match commitsResult with\n                                | Ok commitsValue ->\n                                    let combined =\n                                        Seq.append checkpointsValue.ReturnValue commitsValue.ReturnValue\n                                        |> Seq.sortByDescending (fun reference -> reference.CreatedAt)\n                                        |> Seq.take maxCount\n                                        |> Seq.toArray\n\n                                    return Ok(GraceReturnValue.Create combined (getCorrelationId parseResult))\n                                | Error error -> return Error error\n                            | Error error -> return Error error\n                        }\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"Checkpoints\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type GetSaves() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n                    let query (parameters: GetReferencesParameters) = task { return! Branch.GetSaves(parameters) }\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"Saves\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type GetTags() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n                    let query (parameters: GetReferencesParameters) = task { return! Branch.GetTags(parameters) }\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"Tags\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type GetExternals() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let maxCount = parseResult.GetValue(Options.maxCount)\n                    let query (parameters: GetReferencesParameters) = task { return! Branch.GetExternals(parameters) }\n\n                    let! result = getReferenceHandler parseResult maxCount query\n                    return renderReferencesOutput parseResult \"Externals\" result\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type SwitchParameters() =\n        member val ToBranchId: string = String.Empty with get, set\n        member val ToBranchName: string = String.Empty with get, set\n        member val Sha256Hash: string = String.Empty with get, set\n        member val ReferenceId: string = String.Empty with get, set\n\n    type Switch() =\n        inherit AsynchronousCommandLineAction()\n\n        let switchHandler (parseResult: ParseResult) (switchParameters: SwitchParameters) =\n            task {\n                try\n                    let graceIds = getNormalizedIdsAndNames parseResult\n\n                    /// The GraceStatus at the beginning of running this command.\n                    let mutable previousGraceStatus = GraceStatus.Default\n                    /// The GraceStatus after the current version is saved.\n                    let mutable newGraceStatus = GraceStatus.Default\n                    /// The DirectoryId of the root directory version.\n                    let mutable rootDirectoryId = DirectoryVersionId.Empty\n                    /// The SHA-256 hash of the root directory version.\n                    let mutable rootDirectorySha256Hash = Sha256Hash String.Empty\n                    /// The set of DirectoryIds in the working directory after the current version is saved.\n                    let mutable directoryIdsInNewGraceStatus: HashSet<DirectoryVersionId> = null\n\n                    let showOutput = parseResult |> hasOutput\n\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    // Validate the incoming parameters.\n                    let validateIncomingParameters (showOutput, parseResult: ParseResult, parameters: SwitchParameters) =\n                        let ``Either ToBranchId or ToBranchName must be provided if no Sha256Hash or ReferenceId`` (parseResult: ParseResult) =\n                            oneOfTheseOptionsMustBeProvided\n                                parseResult\n                                [|\n                                    Options.toBranchId\n                                    Options.toBranchName\n                                    Options.sha256Hash\n                                    Options.referenceId\n                                |]\n                                BranchError.EitherToBranchIdOrToBranchNameIsRequired\n\n                        match parseResult\n                              |> CommonValidations\n                              >>= ``Either ToBranchId or ToBranchName must be provided if no Sha256Hash or ReferenceId``\n                            with\n                        | Ok result ->\n                            Ok(showOutput, parseResult, parameters)\n                            |> returnTask\n                        | Error error -> Error error |> returnTask\n\n                    // 0. Get the branchDto for the current branch.\n                    let getCurrentBranch (t: ProgressTask) (showOutput, parseResult: ParseResult, parameters: SwitchParameters) =\n                        task {\n                            t |> startProgressTask showOutput\n\n                            let getParameters =\n                                GetBranchParameters(\n                                    OwnerId = $\"{Current().OwnerId}\",\n                                    OrganizationId = $\"{Current().OrganizationId}\",\n                                    RepositoryId = $\"{Current().RepositoryId}\",\n                                    BranchId = $\"{Current().BranchId}\",\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            match! Branch.Get(getParameters) with\n                            | Ok returnValue ->\n                                t |> setProgressTaskValue showOutput 100.0\n                                let branchDto = returnValue.ReturnValue\n                                return Ok(showOutput, parseResult, parameters, branchDto)\n                            | Error error ->\n                                t |> setProgressTaskValue showOutput 50.0\n                                return Error error\n                        }\n\n                    // 1. Read the Grace status file.\n                    let readGraceStatusFile (t: ProgressTask) (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto) =\n                        task {\n                            t |> startProgressTask showOutput\n                            let! existingGraceStaus = readGraceStatusFile ()\n                            previousGraceStatus <- existingGraceStaus\n                            newGraceStatus <- existingGraceStaus\n                            t |> setProgressTaskValue showOutput 100.0\n                            return Ok(showOutput, parseResult, parameters, currentBranch)\n                        }\n\n                    // 2. Scan the working directory for differences.\n                    let scanForDifferences (t: ProgressTask) (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto) =\n                        task {\n                            t |> startProgressTask showOutput\n\n                            let! differences =\n                                if currentBranch.SaveEnabled then\n                                    scanForDifferences newGraceStatus\n                                else\n                                    List<FileSystemDifference>() |> returnTask\n\n                            t |> setProgressTaskValue showOutput 100.0\n                            return Ok(showOutput, parseResult, parameters, currentBranch, differences)\n                        }\n\n                    // 3. Create new directory versions.\n                    let getNewGraceStatusAndDirectoryVersions\n                        (t: ProgressTask)\n                        (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto, differences: List<FileSystemDifference>)\n                        =\n                        task {\n                            t |> startProgressTask showOutput\n                            let mutable newDirectoryVersions = List<LocalDirectoryVersion>()\n\n                            if currentBranch.SaveEnabled then\n                                let! (updatedGraceStatus, newVersions) = getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n                                newGraceStatus <- updatedGraceStatus\n                                newDirectoryVersions <- newVersions\n\n                            rootDirectoryId <- newGraceStatus.RootDirectoryId\n                            rootDirectorySha256Hash <- newGraceStatus.RootDirectorySha256Hash\n                            directoryIdsInNewGraceStatus <- newGraceStatus.Index.Keys.ToHashSet()\n                            t |> setProgressTaskValue showOutput 100.0\n                            return Ok(showOutput, parseResult, parameters, currentBranch, differences, newDirectoryVersions)\n                        }\n\n                    // 4. Upload changed files to object storage.\n                    let uploadChangedFilesToObjectStorage\n                        (t: ProgressTask)\n                        (showOutput,\n                         parseResult: ParseResult,\n                         parameters: SwitchParameters,\n                         currentBranch: BranchDto,\n                         differences: List<FileSystemDifference>,\n                         newDirectoryVersions: List<LocalDirectoryVersion>)\n                        =\n                        task {\n                            t |> startProgressTask showOutput\n\n                            if currentBranch.SaveEnabled\n                               && newDirectoryVersions.Any() then\n                                let updatedRelativePaths =\n                                    differences\n                                        .Select(fun difference ->\n                                            match difference.DifferenceType with\n                                            | Add ->\n                                                match difference.FileSystemEntryType with\n                                                | FileSystemEntryType.File -> Some difference.RelativePath\n                                                | FileSystemEntryType.Directory -> None\n                                            | Change ->\n                                                match difference.FileSystemEntryType with\n                                                | FileSystemEntryType.File -> Some difference.RelativePath\n                                                | FileSystemEntryType.Directory -> None\n                                            | Delete -> None)\n                                        .Where(fun relativePathOption -> relativePathOption.IsSome)\n                                        .Select(fun relativePath -> relativePath.Value)\n\n                                let newFileVersions =\n                                    updatedRelativePaths.Select (fun relativePath ->\n                                        newDirectoryVersions\n                                            .First(fun dv -> dv.Files.Exists(fun file -> file.RelativePath = relativePath))\n                                            .Files.First(fun file -> file.RelativePath = relativePath))\n\n                                logToAnsiConsole\n                                    Colors.Verbose\n                                    $\"Uploading {newFileVersions.Count()} file(s) from {newDirectoryVersions.Count} new directory version(s) to object storage.\"\n\n                                let getUploadMetadataForFilesParameters =\n                                    Storage.GetUploadMetadataForFilesParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = graceIds.RepositoryIdString,\n                                        RepositoryName = graceIds.RepositoryName,\n                                        CorrelationId = getCorrelationId parseResult,\n                                        FileVersions =\n                                            (newFileVersions\n                                             |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                             |> Seq.toArray)\n                                    )\n\n                                match! uploadFilesToObjectStorage getUploadMetadataForFilesParameters with\n                                | Ok returnValue ->\n                                    t |> setProgressTaskValue showOutput 100.0\n                                    return Ok(showOutput, parseResult, parameters, currentBranch, newDirectoryVersions)\n                                | Error error ->\n                                    t |> setProgressTaskValue showOutput 50.0\n                                    return Error error\n                            else\n                                t |> setProgressTaskValue showOutput 100.0\n                                return Ok(showOutput, parseResult, parameters, currentBranch, newDirectoryVersions)\n                        }\n\n                    // 5. Upload new directory versions.\n                    let uploadNewDirectoryVersions\n                        (t: ProgressTask)\n                        (showOutput,\n                         parseResult: ParseResult,\n                         parameters: SwitchParameters,\n                         currentBranch: BranchDto,\n                         newDirectoryVersions: List<LocalDirectoryVersion>)\n                        =\n                        task {\n                            t |> startProgressTask showOutput\n\n                            if currentBranch.SaveEnabled\n                               && newDirectoryVersions.Any() then\n                                let saveParameters = SaveDirectoryVersionsParameters()\n                                saveParameters.OwnerId <- graceIds.OwnerIdString\n                                saveParameters.OwnerName <- graceIds.OwnerName\n                                saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                                saveParameters.OrganizationName <- graceIds.OrganizationName\n                                saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                                saveParameters.RepositoryName <- graceIds.RepositoryName\n                                saveParameters.CorrelationId <- getCorrelationId parseResult\n\n                                saveParameters.DirectoryVersions <-\n                                    newDirectoryVersions\n                                        .Select(fun dv -> dv.ToDirectoryVersion)\n                                        .ToList()\n\n                                let! uploadDirectoryVersions = DirectoryVersion.SaveDirectoryVersions saveParameters\n\n                                match! DirectoryVersion.SaveDirectoryVersions saveParameters with\n                                | Ok returnValue ->\n                                    t |> setProgressTaskValue showOutput 100.0\n\n                                    return Ok(showOutput, parseResult, parameters, currentBranch, $\"Save created prior to branch switch.\")\n                                | Error error ->\n                                    t |> setProgressTaskValue showOutput 50.0\n                                    return Error error\n                            else\n                                t |> setProgressTaskValue showOutput 100.0\n\n                                return Ok(showOutput, parseResult, parameters, currentBranch, $\"Save created prior to branch switch.\")\n                        }\n\n                    // 6. Create a before save reference.\n                    let createSaveReference\n                        (t: ProgressTask)\n                        (showOutput, parseResult: ParseResult, parameters: SwitchParameters, branchDto: BranchDto, message: string)\n                        =\n                        task {\n                            t |> startProgressTask showOutput\n\n                            if parseResult |> verbose then\n                                logToAnsiConsole\n                                    Colors.Verbose\n                                    $\"In createSaveReference: BranchName: {branchDto.BranchName}; message: {message}; SaveEnabled = {branchDto.SaveEnabled}.\"\n\n                            if branchDto.SaveEnabled then\n                                match! createSaveReference newGraceStatus.Index[rootDirectoryId] message (getCorrelationId parseResult) with\n                                | Ok returnValue ->\n                                    if parseResult |> verbose then\n                                        logToAnsiConsole\n                                            Colors.Verbose\n                                            $\"In createSaveReference: BranchName: {branchDto.BranchName}; message: {message}; Succeeded.\"\n\n                                    t |> setProgressTaskValue showOutput 100.0\n                                    return Ok(showOutput, parseResult, parameters, branchDto)\n                                | Error error ->\n                                    if parseResult |> verbose then\n                                        logToAnsiConsole\n                                            Colors.Verbose\n                                            $\"In createSaveReference: BranchName: {branchDto.BranchName}; message: {message}; Error: {error}.\"\n\n                                    t |> setProgressTaskValue showOutput 50.0\n                                    return Error error\n                            else\n                                t |> setProgressTaskValue showOutput 100.0\n                                return Ok(showOutput, parseResult, parameters, branchDto)\n                        }\n\n                    /// 7. Get the branch and directory versions for the requested version we're switching to from the server.\n                    let getVersionToSwitchToFromBranch\n                        (t: ProgressTask)\n                        (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto)\n                        =\n                        task {\n                            let getNewBranchParameters =\n                                GetBranchParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    BranchId = switchParameters.ToBranchId,\n                                    BranchName = switchParameters.ToBranchName,\n                                    Sha256Hash = switchParameters.Sha256Hash,\n                                    ReferenceId = switchParameters.ReferenceId,\n                                    CorrelationId = graceIds.CorrelationId\n                                )\n\n                            if parseResult |> verbose then\n                                logToAnsiConsole Colors.Verbose $\"In getVersionToSwitchTo: getNewBranchParameters: {serialize getNewBranchParameters}.\"\n\n                            let! newBranchResult = Branch.Get(getNewBranchParameters)\n\n                            match newBranchResult with\n                            | Error error -> return Error error\n                            | Ok returnValue ->\n                                let newBranch = returnValue.ReturnValue\n\n                                if parseResult |> verbose then\n                                    logToAnsiConsole Colors.Verbose $\"In getVersionToSwitchTo: New branch: {serialize newBranch}.\"\n\n                                let getBranchVersionParameters =\n                                    GetBranchVersionParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = $\"{newBranch.RepositoryId}\",\n                                        BranchId = $\"{newBranch.BranchId}\",\n                                        ReferenceId = switchParameters.ReferenceId,\n                                        Sha256Hash = switchParameters.Sha256Hash,\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                if parseResult |> verbose then\n                                    logToAnsiConsole\n                                        Colors.Verbose\n                                        $\"In getVersionToSwitchTo: getBranchVersionParameters: {serialize getBranchVersionParameters}.\"\n\n                                let! versionResult = Branch.GetVersion getBranchVersionParameters\n\n                                match versionResult with\n                                | Error error -> return Error error\n                                | Ok returnValue ->\n                                    let directoryIds = returnValue.ReturnValue\n\n                                    if parseResult |> verbose then\n                                        logToAnsiConsole\n                                            Colors.Verbose\n                                            $\"Retrieved {directoryIds.Count()} directory version(s) for branch {newBranch.BranchName}.\"\n\n                                    t |> setProgressTaskValue showOutput 100.0\n                                    return Ok(showOutput, parseResult, parameters, currentBranch, newBranch, directoryIds)\n                        }\n\n                    let getVersionToSwitchToFromReference\n                        (t: ProgressTask)\n                        (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto)\n                        =\n                        task {\n                            let getReferenceParameters =\n                                GetReferenceParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    BranchId = graceIds.BranchIdString,\n                                    BranchName = graceIds.BranchName,\n                                    ReferenceId = switchParameters.ReferenceId,\n                                    CorrelationId = graceIds.CorrelationId\n                                )\n\n                            let! referenceResult = Branch.GetReference(getReferenceParameters)\n\n                            match referenceResult with\n                            | Error error -> return Error error\n                            | Ok returnValue ->\n                                // We have the reference, let's get the new branch and the DirectoryVersion from the reference.\n                                let reference = returnValue.ReturnValue\n\n                                let getNewBranchParameters =\n                                    GetBranchParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = graceIds.RepositoryIdString,\n                                        RepositoryName = graceIds.RepositoryName,\n                                        BranchId = $\"{reference.BranchId}\",\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                let! getNewBranchResult = Branch.Get(getNewBranchParameters)\n\n                                let getVersionParameters =\n                                    GetBranchVersionParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = graceIds.RepositoryIdString,\n                                        RepositoryName = graceIds.RepositoryName,\n                                        BranchId = $\"{reference.BranchId}\",\n                                        ReferenceId = switchParameters.ReferenceId,\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                let! getVersionResult = Branch.GetVersion getVersionParameters\n\n                                match (getNewBranchResult, getVersionResult) with\n                                | Ok branchReturnValue, Ok versionReturnValue ->\n                                    let newBranch = branchReturnValue.ReturnValue\n                                    let directoryIds = versionReturnValue.ReturnValue\n                                    t |> setProgressTaskValue showOutput 100.0\n                                    return Ok(showOutput, parseResult, parameters, currentBranch, newBranch, directoryIds)\n                                | Error error, _ -> return Error error\n                                | _, Error error -> return Error error\n                        }\n\n                    let getVersionToSwitchToFromSha\n                        (t: ProgressTask)\n                        (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto)\n                        =\n                        task {\n                            let getVersionParameters =\n                                GetBranchVersionParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    BranchId = $\"{currentBranch.BranchId}\",\n                                    Sha256Hash = switchParameters.Sha256Hash,\n                                    CorrelationId = graceIds.CorrelationId\n                                )\n\n                            let! versionResult = Branch.GetVersion getVersionParameters\n\n                            match versionResult with\n                            | Ok returnValue ->\n                                let directoryIds = returnValue.ReturnValue\n                                t |> setProgressTaskValue showOutput 100.0\n                                return Ok(showOutput, parseResult, parameters, currentBranch, currentBranch, directoryIds)\n                            | Error error -> return Error error\n                        }\n\n                    let getVersionToSwitchTo (t: ProgressTask) (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto) =\n                        t |> startProgressTask showOutput\n\n                        if not\n                           <| String.IsNullOrEmpty(switchParameters.ToBranchId)\n                           || not\n                              <| String.IsNullOrEmpty(switchParameters.ToBranchName) then\n                            getVersionToSwitchToFromBranch t (showOutput, parseResult, parameters, currentBranch)\n                        elif\n                            not\n                            <| String.IsNullOrEmpty(switchParameters.ReferenceId)\n                        then\n                            getVersionToSwitchToFromReference t (showOutput, parseResult, parameters, currentBranch)\n                        elif\n                            not\n                            <| String.IsNullOrEmpty(switchParameters.Sha256Hash)\n                        then\n                            getVersionToSwitchToFromSha t (showOutput, parseResult, parameters, currentBranch)\n                        else\n                            Task.FromResult(\n                                Error(\n                                    GraceError.Create (getErrorMessage BranchError.EitherToBranchIdOrToBranchNameIsRequired) (parseResult |> getCorrelationId)\n                                )\n                            )\n\n                    let getMissingDirectoryVersionsWithClosure (missingDirectoryIds: IEnumerable<DirectoryVersionId>) =\n                        task {\n                            let knownDirectoryIds = HashSet<DirectoryVersionId>(directoryIdsInNewGraceStatus)\n                            let fetchedDirectoryVersions = Dictionary<DirectoryVersionId, DirectoryVersionDto>()\n                            let pendingDirectoryIds = Queue<DirectoryVersionId>()\n\n                            missingDirectoryIds\n                            |> Seq.distinct\n                            |> Seq.iter (fun directoryId -> pendingDirectoryIds.Enqueue(directoryId))\n\n                            let mutable resultError: GraceError option = None\n\n                            while pendingDirectoryIds.Count > 0\n                                  && resultError.IsNone do\n                                let batch = List<DirectoryVersionId>()\n\n                                while pendingDirectoryIds.Count > 0 do\n                                    let directoryId = pendingDirectoryIds.Dequeue()\n\n                                    if\n                                        not (knownDirectoryIds.Contains(directoryId))\n                                        && not (fetchedDirectoryVersions.ContainsKey(directoryId))\n                                    then\n                                        batch.Add(directoryId)\n\n                                if batch.Count > 0 then\n                                    let getByDirectoryIdParameters =\n                                        GetByDirectoryIdsParameters(\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OwnerName = graceIds.OwnerName,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            OrganizationName = graceIds.OrganizationName,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            RepositoryName = graceIds.RepositoryName,\n                                            DirectoryVersionId = $\"{rootDirectoryId}\",\n                                            DirectoryIds = batch,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    match! DirectoryVersion.GetByDirectoryIds getByDirectoryIdParameters with\n                                    | Ok returnValue ->\n                                        let fetchedBatch = returnValue.ReturnValue |> Seq.toArray\n                                        let fetchedBatchIds = HashSet<DirectoryVersionId>()\n\n                                        fetchedBatch\n                                        |> Seq.iter (fun directoryVersionDto ->\n                                            let directoryVersion = directoryVersionDto.DirectoryVersion\n\n                                            fetchedBatchIds.Add(directoryVersion.DirectoryVersionId)\n                                            |> ignore\n\n                                            knownDirectoryIds.Add(directoryVersion.DirectoryVersionId)\n                                            |> ignore\n\n                                            fetchedDirectoryVersions[directoryVersion.DirectoryVersionId] <- directoryVersionDto)\n\n                                        let unresolvedRequestedIds =\n                                            batch\n                                                .Where(fun requestedId ->\n                                                    not (fetchedBatchIds.Contains(requestedId))\n                                                    && not (knownDirectoryIds.Contains(requestedId)))\n                                                .ToArray()\n\n                                        if unresolvedRequestedIds.Length > 0 then\n                                            let unresolvedRequestedIdsText =\n                                                unresolvedRequestedIds\n                                                |> Seq.map (fun directoryId -> $\"{directoryId}\")\n                                                |> String.concat \", \"\n\n                                            resultError <-\n                                                Some(\n                                                    GraceError.Create\n                                                        $\"Failed switching branches because the server did not return required DirectoryVersionId values: {unresolvedRequestedIdsText}.\"\n                                                        (parseResult |> getCorrelationId)\n                                                )\n                                        else\n                                            fetchedBatch\n                                            |> Seq.collect (fun directoryVersionDto -> directoryVersionDto.DirectoryVersion.Directories)\n                                            |> Seq.iter (fun childDirectoryId ->\n                                                if\n                                                    not (knownDirectoryIds.Contains(childDirectoryId))\n                                                    && not (fetchedDirectoryVersions.ContainsKey(childDirectoryId))\n                                                then\n                                                    pendingDirectoryIds.Enqueue(childDirectoryId))\n                                    | Error error -> resultError <- Some error\n\n                            match resultError with\n                            | Some error -> return Error error\n                            | None -> return Ok(fetchedDirectoryVersions.Values |> Seq.toArray :> IEnumerable<DirectoryVersionDto>)\n                        }\n\n                    /// 8. Update object cache and working directory.\n                    let updateWorkingDirectory\n                        (t: ProgressTask)\n                        (showOutput,\n                         parseResult: ParseResult,\n                         parameters: SwitchParameters,\n                         currentBranch: BranchDto,\n                         newBranch: BranchDto,\n                         directoryIds: IEnumerable<DirectoryVersionId>)\n                        =\n                        task {\n                            t |> startProgressTask showOutput\n\n                            let missingDirectoryIds =\n                                directoryIds\n                                    .Where(fun directoryId ->\n                                        not\n                                        <| directoryIdsInNewGraceStatus.Contains(directoryId))\n                                    .ToList()\n\n                            if parseResult |> verbose then\n                                logToAnsiConsole Colors.Verbose $\"In updateWorkingDirectory: missingDirectoryIds.Count: {missingDirectoryIds.Count()}.\"\n\n                            match! getMissingDirectoryVersionsWithClosure missingDirectoryIds with\n                            | Ok newDirectoryVersionDtos ->\n                                // Create a new version of GraceStatus that includes the new DirectoryVersions.\n                                if parseResult |> verbose then\n                                    logToAnsiConsole Colors.Verbose $\"In updateWorkingDirectory: newDirectoryVersions.Count: {newDirectoryVersionDtos.Count()}.\"\n\n                                let graceStatusWithNewDirectoryVersionsFromServer =\n                                    updateGraceStatusWithNewDirectoryVersionsFromServer newGraceStatus newDirectoryVersionDtos\n\n                                let mutable isError = false\n\n                                // Identify files that we don't already have in object cache and download them.\n                                let getDownloadUriParameters =\n                                    Storage.GetDownloadUriParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = graceIds.RepositoryIdString,\n                                        RepositoryName = graceIds.RepositoryName,\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                for directoryVersion in graceStatusWithNewDirectoryVersionsFromServer.Index.Values do\n                                    match! (downloadFilesFromObjectStorage getDownloadUriParameters directoryVersion.Files (getCorrelationId parseResult)) with\n                                    | Ok _ ->\n                                        try\n                                            //logToAnsiConsole Colors.Verbose $\"Succeeded downloading files from object storage for {directoryVersion.RelativePath}.\"\n\n                                            // Write the UpdatesInProgress file to let grace watch know to ignore these changes.\n                                            // This file is deleted in the finally clause.\n                                            do! File.WriteAllTextAsync(updateInProgressFileName (), \"`grace switch` is in progress.\")\n\n                                            // Update working directory based on new GraceStatus.Index\n                                            do!\n                                                updateWorkingDirectory\n                                                    newGraceStatus\n                                                    graceStatusWithNewDirectoryVersionsFromServer\n                                                    newDirectoryVersionDtos\n                                                    (getCorrelationId parseResult)\n                                            //logToAnsiConsole Colors.Verbose $\"Succeeded calling updateWorkingDirectory.\"\n\n                                            // Save the new Grace Status.\n                                            do! writeGraceStatusFile graceStatusWithNewDirectoryVersionsFromServer\n\n                                            // Update graceconfig.json.\n                                            let configuration = Current()\n                                            configuration.BranchId <- newBranch.BranchId\n                                            configuration.BranchName <- newBranch.BranchName\n                                            updateConfiguration configuration\n                                            t |> setProgressTaskValue showOutput 100.0\n                                        finally\n                                            // Delete the UpdatesInProgress file.\n                                            File.Delete(updateInProgressFileName ())\n\n                                    | Error error ->\n                                        if parseResult |> verbose then\n                                            logToAnsiConsole Colors.Verbose $\"Failed downloading files from object storage for {directoryVersion.RelativePath}.\"\n\n                                        logToAnsiConsole Colors.Error $\"{error}\"\n                                        isError <- true\n\n                                if not <| isError then\n                                    newGraceStatus <- graceStatusWithNewDirectoryVersionsFromServer\n                                    rootDirectoryId <- newGraceStatus.RootDirectoryId\n                                    rootDirectorySha256Hash <- newGraceStatus.RootDirectorySha256Hash\n                                    directoryIdsInNewGraceStatus <- newGraceStatus.Index.Keys.ToHashSet()\n\n                                    if parseResult |> verbose then\n                                        logToAnsiConsole Colors.Verbose $\"About to exit updateWorkingDirectory.\"\n\n                                    return Ok(showOutput, parseResult, parameters, newBranch, $\"Save created after branch switch.\")\n                                else\n                                    return Error(GraceError.Create $\"Failed downloading files from object storage.\" (parseResult |> getCorrelationId))\n                            | Error error ->\n                                if parseResult |> verbose then\n                                    logToAnsiConsole Colors.Verbose $\"Failed retrieving directory versions for switch.\"\n\n                                logToAnsiConsole Colors.Error $\"{error}\"\n                                return Error(GraceError.Create $\"{error}\" (parseResult |> getCorrelationId))\n                        }\n\n                    let writeNewGraceStatus (t: ProgressTask) (showOutput, parseResult: ParseResult, parameters: SwitchParameters, currentBranch: BranchDto) =\n                        task {\n                            t |> startProgressTask showOutput\n                            do! writeGraceStatusFile newGraceStatus\n                            do! upsertObjectCache newGraceStatus.Index.Values\n\n                            t |> setProgressTaskValue showOutput 100.0\n                            return Ok(showOutput, parseResult, parameters, currentBranch)\n                        }\n\n                    let generateResult (progressTasks: ProgressTask array) =\n                        task {\n                            let! result =\n                                (showOutput, parseResult, switchParameters)\n                                |> validateIncomingParameters\n                                >>=! getCurrentBranch progressTasks[0]\n                                >>=! readGraceStatusFile progressTasks[1]\n                                >>=! scanForDifferences progressTasks[2]\n                                >>=! getNewGraceStatusAndDirectoryVersions progressTasks[3]\n                                >>=! uploadChangedFilesToObjectStorage progressTasks[4]\n                                >>=! uploadNewDirectoryVersions progressTasks[5]\n                                >>=! createSaveReference progressTasks[6]\n                                >>=! getVersionToSwitchTo progressTasks[7]\n                                >>=! updateWorkingDirectory progressTasks[8]\n                                >>=! createSaveReference progressTasks[9]\n                                >>=! writeNewGraceStatus progressTasks[10]\n\n                            match result with\n                            | Ok _ -> return 0\n                            | Error error ->\n                                if parseResult |> verbose then\n                                    AnsiConsole.MarkupLine($\"[{Colors.Error}]{error}[/]\")\n                                else\n                                    AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error}[/]\")\n\n                                return -1\n                        }\n\n                    if showOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString GettingCurrentBranch}[/]\", autoStart = false)\n\n                                        let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString ReadingGraceStatus}[/]\", autoStart = false)\n\n                                        let t2 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString ScanningWorkingDirectory}[/]\", autoStart = false)\n\n                                        let t3 =\n                                            progressContext.AddTask(\n                                                $\"[{Color.DodgerBlue1}]{UIString.getString CreatingNewDirectoryVersions}[/]\",\n                                                autoStart = false\n                                            )\n\n                                        let t4 = progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString UploadingFiles}[/]\", autoStart = false)\n\n                                        let t5 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString SavingDirectoryVersions}[/]\", autoStart = false)\n\n                                        let t6 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString CreatingSaveReference}[/]\", autoStart = false)\n\n                                        let t7 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString GettingLatestVersion}[/]\", autoStart = false)\n\n                                        let t8 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString UpdatingWorkingDirectory}[/]\", autoStart = false)\n\n                                        let t9 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString CreatingSaveReference}[/]\", autoStart = false)\n\n                                        let t10 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]{UIString.getString WritingGraceStatusFile}[/]\", autoStart = false)\n\n                                        return!\n                                            generateResult [| t0\n                                                              t1\n                                                              t2\n                                                              t3\n                                                              t4\n                                                              t5\n                                                              t6\n                                                              t7\n                                                              t8\n                                                              t9\n                                                              t10 |]\n                                    })\n                    else\n                        // If we're not showing output, we don't need to create the progress tasks.\n                        return!\n                            generateResult [| emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask\n                                              emptyTask |]\n                with\n                | ex ->\n                    logToConsole $\"{ExceptionResponse.Create ex}\"\n                    logToAnsiConsole Colors.Error (Markup.Escape($\"{ExceptionResponse.Create ex}\"))\n                    logToAnsiConsole Colors.Important $\"CorrelationId: {(parseResult |> getCorrelationId)}\"\n                    return -1\n            }\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName ()))\n                |> ignore\n\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    do! File.WriteAllTextAsync(updateInProgressFileName (), \"`grace switch` is in progress.\")\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let switchParameters = SwitchParameters()\n\n                    let toBranchId = parseResult.GetValue(Options.toBranchId)\n                    if toBranchId <> Guid.Empty then switchParameters.ToBranchId <- $\"{toBranchId}\"\n\n                    let toBranchName = parseResult.GetValue(Options.toBranchName)\n                    switchParameters.ToBranchName <- toBranchName\n\n                    let referenceId = parseResult.GetValue(Options.referenceId)\n\n                    if referenceId <> Guid.Empty then\n                        switchParameters.ReferenceId <- $\"{referenceId}\"\n\n                    let sha256Hash = parseResult.GetValue(Options.sha256Hash)\n                    switchParameters.Sha256Hash <- sha256Hash\n\n                    let! result = switchHandler parseResult switchParameters\n                    return result\n                finally\n                    if File.Exists(updateInProgressFileName ()) then\n                        File.Delete(updateInProgressFileName ())\n            }\n\n    let rebaseHandler (graceIds: GraceIds) (graceStatus: GraceStatus) =\n        task {\n            // --------------------------------------------------------------------------------------------------------------------------------------\n            // Algorithm:\n            //\n            // Get a diff between the promotion from the parent branch that the current branch is based on, and the latest promotion from the parent branch.\n            //   These are the changes that we expect to apply to the current branch.\n            //\n            // Get a diff between the latest reference on this branch and the promotion that it's based on from the parent branch.\n            //   This will be what's changed in the current branch since it was last rebased.\n            //\n            // If a file has changed in the first diff, but not in the second diff, cool, we can automatically copy them.\n            // If a file has changed in the second diff, but not in the first diff, cool, we can keep those changes.\n            // If a file has changed in both, we have a promotion conflict, so we'll call an LLM to suggest a resolution.\n            //\n            // Then we call Branch.Rebase() to actually record the update.\n            // --------------------------------------------------------------------------------------------------------------------------------------\n\n            logToAnsiConsole Colors.Verbose $\"In Branch.CLI.rebaseHandler: GraceIds:{Environment.NewLine}{serialize graceIds}\"\n\n            // First, get the current branchDto so we have the latest promotion that it's based on.\n            let branchGetParameters =\n                GetBranchParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            match! Branch.Get(branchGetParameters) with\n            | Ok returnValue ->\n                let branchDto = returnValue.ReturnValue\n\n                // Now, get the parent branch information so we have its latest promotion.\n                match! Branch.GetParentBranch(branchGetParameters) with\n                | Ok returnValue ->\n                    let parentBranchDto = returnValue.ReturnValue\n\n                    if branchDto.BasedOn.ReferenceId = parentBranchDto.LatestPromotion.ReferenceId then\n                        AnsiConsole.MarkupLine(\"The current branch is already based on the latest promotion in the parent branch.\")\n\n                        AnsiConsole.MarkupLine(\"Run `grace status` to see more.\")\n                        return 0\n                    else\n                        // Now, get ReferenceDtos for current.BasedOn and parent.LatestPromotion so we have their DirectoryId's.\n                        let latestCommit = branchDto.LatestCommit\n                        let parentLatestPromotion = parentBranchDto.LatestPromotion\n                        let basedOn = branchDto.BasedOn\n\n                        // Get the latest reference from the current branch.\n                        let getReferencesParameters =\n                            Parameters.Branch.GetReferencesParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                BranchId = graceIds.BranchIdString,\n                                MaxCount = 1,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n                        //logToAnsiConsole Colors.Verbose $\"getReferencesParameters: {getReferencesParameters |> serialize)}\"\n                        match! Branch.GetReferences(getReferencesParameters) with\n                        | Ok returnValue ->\n                            let latestReference =\n                                if returnValue.ReturnValue.Count() > 0 then\n                                    returnValue.ReturnValue.First()\n                                else\n                                    ReferenceDto.Default\n                            //logToAnsiConsole Colors.Verbose $\"latestReference: {serialize latestReference}\"\n                            // Now we have all of the references we need, so we have DirectoryId's to do diffs with.\n\n                            let! (diffs, errors) =\n                                task {\n                                    if basedOn.DirectoryId <> DirectoryVersionId.Empty then\n                                        // First diff: parent promotion that current branch is based on vs. parent's latest promotion.\n                                        let diffParameters =\n                                            Parameters.Diff.GetDiffParameters(\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                RepositoryId = $\"{branchDto.RepositoryId}\",\n                                                DirectoryVersionId1 = basedOn.DirectoryId,\n                                                DirectoryVersionId2 = parentLatestPromotion.DirectoryId,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n                                        //logToAnsiConsole Colors.Verbose $\"First diff: {Markup.Escape(serialize diffParameters)}\"\n                                        let! firstDiff = Diff.GetDiff(diffParameters)\n\n                                        // Second diff: latest reference on current branch vs. parent promotion that current branch is based on.\n                                        let diffParameters =\n                                            Parameters.Diff.GetDiffParameters(\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                RepositoryId = $\"{branchDto.RepositoryId}\",\n                                                DirectoryVersionId1 = latestReference.DirectoryId,\n                                                DirectoryVersionId2 = basedOn.DirectoryId,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n                                        //logToAnsiConsole Colors.Verbose $\"Second diff: {Markup.Escape(serialize diffParameters)}\"\n                                        let! secondDiff = Diff.GetDiff(diffParameters)\n\n                                        let returnValue =\n                                            Result.partition [ firstDiff\n                                                               secondDiff ]\n\n                                        return returnValue\n                                    else\n                                        // This should only happen when first creating a repository, when main has no promotions.\n                                        // Only one diff possible: latest reference on current branch vs. parent's latest promotion.\n                                        let diffParameters =\n                                            Parameters.Diff.GetDiffParameters(\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                RepositoryId = $\"{branchDto.RepositoryId}\",\n                                                DirectoryVersionId1 = latestReference.DirectoryId,\n                                                DirectoryVersionId2 = parentLatestPromotion.DirectoryId,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n                                        //logToAnsiConsole Colors.Verbose $\"Initial diff: {Markup.Escape(serialize diffParameters)}\"\n                                        let! diff = Diff.GetDiff(diffParameters)\n                                        let returnValue = Result.partition [ diff ]\n                                        return returnValue\n                                }\n\n                            // So, right now, if repo just created, and BasedOn is empty, we'll have a single diff.\n                            // That fails a few lines below here.\n                            // Have to decide what to do in this case.\n\n                            if errors.Count() = 0 then\n                                // Yay! We have our two diffs.\n                                let diff1 = diffs[0].ReturnValue\n                                let diff2 = diffs[1].ReturnValue\n\n                                let filesToDownload = List<FileSystemDifference>()\n\n                                // Identify which files have been changed in the first diff, but not in the second diff.\n                                //   We can just download and copy these files into place in the working directory.\n                                for fileDifference in diff1.Differences do\n                                    if not\n                                       <| diff2.Differences.Any(fun d -> d.RelativePath = fileDifference.RelativePath) then\n                                        // Copy different file version into place - similar to how we do it for switch\n                                        filesToDownload.Add(fileDifference)\n\n                                let getParentLatestPromotionDirectoryParameters =\n                                    Parameters.DirectoryVersion.GetParameters(\n                                        OwnerId = $\"{branchDto.OwnerId}\",\n                                        OrganizationId = $\"{branchDto.OrganizationId}\",\n                                        RepositoryId = $\"{branchDto.RepositoryId}\",\n                                        DirectoryVersionId = $\"{parentLatestPromotion.DirectoryId}\",\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                let getLatestReferenceDirectoryParameters =\n                                    Parameters.DirectoryVersion.GetParameters(\n                                        OwnerId = $\"{branchDto.OwnerId}\",\n                                        OrganizationId = $\"{branchDto.OrganizationId}\",\n                                        RepositoryId = $\"{branchDto.RepositoryId}\",\n                                        DirectoryVersionId = $\"{latestReference.DirectoryId}\",\n                                        CorrelationId = graceIds.CorrelationId\n                                    )\n\n                                // Get the directory versions for the parent promotion that we're rebasing on, and the latest reference.\n                                let! d1 = DirectoryVersion.GetDirectoryVersionsRecursive(getParentLatestPromotionDirectoryParameters)\n\n                                let! d2 = DirectoryVersion.GetDirectoryVersionsRecursive(getLatestReferenceDirectoryParameters)\n\n                                let createFileVersionLookupDictionary (directoryVersionDtos: IEnumerable<DirectoryVersionDto>) =\n                                    let lookup = Dictionary<RelativePath, LocalFileVersion>(StringComparer.OrdinalIgnoreCase)\n\n                                    directoryVersionDtos\n                                    |> Seq.map (fun dv -> dv.DirectoryVersion)\n                                    |> Seq.map (fun dv -> dv.ToLocalDirectoryVersion(dv.CreatedAt.ToDateTimeUtc()))\n                                    |> Seq.map (fun dv -> dv.Files)\n                                    |> Seq.concat\n                                    |> Seq.iter (fun file ->\n                                        //logToConsole $\"In Branch.CLI.createFileVersionLookupDictionary: Adding to lookup: {file.RelativePath}.\"\n                                        lookup.TryAdd(file.RelativePath, file) |> ignore)\n\n                                    //lookup.GetAlternateLookup()\n                                    lookup\n\n                                let (directories, errors) = Result.partition [ d1; d2 ]\n\n                                if errors.Count() = 0 then\n                                    let parentLatestPromotionDirectoryVersions = directories[0].ReturnValue\n                                    let latestReferenceDirectoryVersions = directories[1].ReturnValue\n\n                                    let parentLatestPromotionLookup = createFileVersionLookupDictionary parentLatestPromotionDirectoryVersions\n\n                                    let latestReferenceLookup = createFileVersionLookupDictionary latestReferenceDirectoryVersions\n\n                                    // Get the specific FileVersions for those files from the contents of the parent's latest promotion.\n                                    let fileVersionsToDownload =\n                                        filesToDownload\n                                        |> Seq.where (fun fileToDownload -> parentLatestPromotionLookup.ContainsKey($\"{fileToDownload.RelativePath}\"))\n                                        |> Seq.map (fun fileToDownload -> parentLatestPromotionLookup[$\"{fileToDownload.RelativePath}\"])\n                                    //logToAnsiConsole Colors.Verbose $\"fileVersionsToDownload: {fileVersionsToDownload.Count()}\"\n                                    //for f in fileVersionsToDownload do\n                                    //    logToAnsiConsole Colors.Verbose  $\"relativePath: {f.RelativePath}\"\n\n                                    // Download those FileVersions from object storage, and copy them into the working directory.\n                                    let getDownloadUriParameters =\n                                        Storage.GetDownloadUriParameters(\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    match! downloadFilesFromObjectStorage getDownloadUriParameters fileVersionsToDownload graceIds.CorrelationId with\n                                    | Ok _ ->\n                                        //logToAnsiConsole Colors.Verbose $\"Succeeded in downloadFilesFromObjectStorage.\"\n                                        fileVersionsToDownload\n                                        |> Seq.iter (fun file ->\n                                            // Delete the existing file in the working directory.\n                                            File.Delete(file.FullName)\n                                            // Copy the version from the object cache to the working directory.\n                                            File.Copy(file.FullObjectPath, file.FullName))\n\n                                    //logToAnsiConsole Colors.Verbose $\"Copied files into place.\"\n                                    | Error error -> AnsiConsole.WriteLine($\"[{Colors.Error}]{Markup.Escape(error)}[/]\")\n\n                                    // If a file has changed in the second diff, but not in the first diff, cool, we can keep those changes, nothing to be done.\n\n                                    // If a file has changed in both, we have to check the two diffs at the line-level to see if there are any conflicts.\n                                    let mutable potentialPromotionConflicts = false\n\n                                    for diff1Difference in diff1.Differences do\n                                        let diff2DifferenceQuery =\n                                            diff2.Differences.Where (fun d ->\n                                                d.RelativePath = diff1Difference.RelativePath\n                                                && d.FileSystemEntryType = FileSystemEntryType.File\n                                                && d.DifferenceType = DifferenceType.Change)\n\n                                        if diff2DifferenceQuery.Count() = 1 then\n                                            // We have a file that's changed in both diffs.\n                                            let diff2Difference = diff2DifferenceQuery.First()\n\n                                            // Check the Sha256Hash values; if they're identical, ignore the file.\n                                            //let fileVersion1 = parentLatestPromotionLookup[$\"{diff1Difference.RelativePath}\"]\n                                            let fileVersion1 =\n                                                parentLatestPromotionLookup.FirstOrDefault(fun kvp -> kvp.Key = $\"{diff1Difference.RelativePath}\")\n                                            //let fileVersion2 = latestReferenceLookup[$\"{diff2Difference.RelativePath}\"]\n                                            let fileVersion2 = latestReferenceLookup.FirstOrDefault(fun kvp -> kvp.Key = $\"{diff2Difference.RelativePath}\")\n                                            //if (not <| isNull(fileVersion1) && not <| isNull(fileVersion2)) && (fileVersion1.Value.Sha256Hash <> fileVersion2.Value.Sha256Hash) then\n                                            if (fileVersion1.Value.Sha256Hash\n                                                <> fileVersion2.Value.Sha256Hash) then\n                                                // Compare them at a line level; if there are no overlapping lines, we can just modify the working-directory version.\n                                                // ...\n                                                // For now, we're just going to show a message.\n                                                AnsiConsole.MarkupLine(\n                                                    $\"[{Colors.Important}]Potential promotion conflict: file {diff1Difference.RelativePath} has been changed in both the latest promotion, and in the current branch.[/]\"\n                                                )\n\n                                                AnsiConsole.MarkupLine(\n                                                    $\"[{Colors.Important}]fileVersion1.Sha256Hash: {fileVersion1.Value.Sha256Hash}; fileVersion1.LastWriteTimeUTC: {fileVersion1.Value.LastWriteTimeUtc}.[/]\"\n                                                )\n\n                                                AnsiConsole.MarkupLine(\n                                                    $\"[{Colors.Important}]fileVersion2.Sha256Hash: {fileVersion2.Value.Sha256Hash}; fileVersion2.LastWriteTimeUTC: {fileVersion2.Value.LastWriteTimeUtc}.[/]\"\n                                                )\n\n                                                potentialPromotionConflicts <- true\n\n                                    /// Create new directory versions and updates Grace Status with them.\n                                    let getNewGraceStatusAndDirectoryVersions\n                                        (\n                                            showOutput,\n                                            graceStatus,\n                                            currentBranch: BranchDto,\n                                            differences: IEnumerable<FileSystemDifference>\n                                        )\n                                        =\n                                        task {\n                                            if differences.Count() > 0 then\n                                                let! (updatedGraceStatus, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions graceStatus differences\n\n                                                return Ok(updatedGraceStatus, newDirectoryVersions)\n                                            else\n                                                return Ok(graceStatus, List<LocalDirectoryVersion>())\n                                        }\n\n                                    /// Upload new DirectoryVersion records to the server.\n                                    let uploadNewDirectoryVersions (currentBranch: BranchDto) (newDirectoryVersions: List<LocalDirectoryVersion>) =\n                                        task {\n                                            if currentBranch.SaveEnabled\n                                               && newDirectoryVersions.Any() then\n                                                let saveParameters = SaveDirectoryVersionsParameters()\n                                                saveParameters.OwnerId <- graceIds.OwnerIdString\n                                                saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                                                saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                                                saveParameters.CorrelationId <- graceIds.CorrelationId\n\n                                                saveParameters.DirectoryVersions <-\n                                                    newDirectoryVersions\n                                                        .Select(fun dv -> dv.ToDirectoryVersion)\n                                                        .ToList()\n\n                                                match! DirectoryVersion.SaveDirectoryVersions saveParameters with\n                                                | Ok returnValue -> return Ok()\n                                                | Error error -> return Error error\n                                            else\n                                                return Ok()\n                                        }\n\n                                    if not <| potentialPromotionConflicts then\n                                        // Yay! No promotion conflicts.\n                                        let mutable newGraceStatus = graceStatus\n\n                                        // Update the GraceStatus file with the new file versions (and therefore new LocalDirectoryVersion's) we just put in place.\n                                        // filesToDownload is, conveniently, the list of files we're changing in the rebase.\n                                        match! getNewGraceStatusAndDirectoryVersions (true, graceStatus, branchDto, filesToDownload) with\n                                        | Ok (updatedGraceStatus, newDirectoryVersions) ->\n                                            // Ensure that previous DirectoryVersions for a given path are deleted from GraceStatus.\n                                            newDirectoryVersions\n                                            |> Seq.iter (fun localDirectoryVersion ->\n                                                let directoryVersionsWithSameRelativePath =\n                                                    updatedGraceStatus.Index.Values.Where(fun dv -> dv.RelativePath = localDirectoryVersion.RelativePath)\n\n                                                if directoryVersionsWithSameRelativePath.Count() > 1 then\n                                                    // Delete all but the most recent DirectoryVersion for this path.\n                                                    directoryVersionsWithSameRelativePath\n                                                    |> Seq.where (fun dv ->\n                                                        dv.DirectoryVersionId\n                                                        <> localDirectoryVersion.DirectoryVersionId)\n                                                    |> Seq.iter (fun dv ->\n                                                        let mutable localDirectoryVersion = LocalDirectoryVersion.Default\n\n                                                        updatedGraceStatus.Index.Remove(dv.DirectoryVersionId, &localDirectoryVersion)\n                                                        |> ignore))\n\n                                            let! result = uploadNewDirectoryVersions branchDto newDirectoryVersions\n                                            do! writeGraceStatusFile updatedGraceStatus\n                                            do! updateGraceWatchInterprocessFile updatedGraceStatus None\n                                            newGraceStatus <- updatedGraceStatus\n\n                                        | Error error -> logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n\n                                        // Create a save reference to mark the state of the branch after rebase.\n                                        let rootDirectoryVersion = getRootDirectoryVersion newGraceStatus\n\n                                        let saveReferenceParameters =\n                                            Parameters.Branch.CreateReferenceParameters(\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                BranchId = graceIds.BranchIdString,\n                                                Sha256Hash = rootDirectoryVersion.Sha256Hash,\n                                                DirectoryVersionId = rootDirectoryVersion.DirectoryVersionId,\n                                                Message =\n                                                    $\"Save after rebase from {parentBranchDto.BranchName}; {getShortSha256Hash parentLatestPromotion.Sha256Hash} - {parentLatestPromotion.ReferenceText}.\"\n                                            )\n\n                                        match! Branch.Save(saveReferenceParameters) with\n                                        | Ok returnValue ->\n                                            // Add a rebase event to the branch.\n                                            let rebaseParameters =\n                                                Parameters.Branch.RebaseParameters(\n                                                    OwnerId = graceIds.OwnerIdString,\n                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                    BranchId = graceIds.BranchIdString,\n                                                    BasedOn = parentLatestPromotion.ReferenceId\n                                                )\n\n                                            match! Branch.Rebase(rebaseParameters) with\n                                            | Ok returnValue ->\n                                                AnsiConsole.MarkupLine($\"[{Colors.Important}]Rebase succeeded.[/]\")\n                                                return 0\n                                            | Error error ->\n                                                logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                                                return -1\n                                        | Error error ->\n                                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                                            return -1\n                                    else\n                                        AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]A potential promotion conflict was detected. Rebase not successful.[/]\")\n\n                                        return -1\n                                else\n                                    logToAnsiConsole Colors.Error (Markup.Escape($\"{errors.First()}\"))\n                                    return -1\n                            else\n                                logToAnsiConsole Colors.Error (Markup.Escape($\"{errors.First()}\"))\n                                return -1\n                        | Error error ->\n                            logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                            return -1\n                | Error error ->\n                    logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                    return -1\n            | Error error ->\n                logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                return -1\n        }\n\n    type Rebase() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName ()))\n                |> ignore\n\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    do! File.WriteAllTextAsync(updateInProgressFileName (), \"`grace rebase` is in progress.\")\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let! graceStatus = readGraceStatusFile ()\n\n                    let! result = rebaseHandler graceIds graceStatus\n                    return result\n                finally\n                    if File.Exists(updateInProgressFileName ()) then\n                        File.Delete(updateInProgressFileName ())\n            }\n\n    type ParentBranchReferencesState =\n        | NoParentBranch\n        | References of ReferenceDto array\n        | FetchError of GraceError\n\n    let private getParentBranchReferencesState (graceIds: GraceIds) (branchDto: BranchDto) =\n        if branchDto.ParentBranchId\n           <> Constants.DefaultParentBranchId then\n            let getParentBranchReferencesParameters =\n                GetReferencesParameters(\n                    BranchId = $\"{branchDto.ParentBranchId}\",\n                    OwnerId = $\"{branchDto.OwnerId}\",\n                    OrganizationId = $\"{branchDto.OrganizationId}\",\n                    RepositoryId = $\"{branchDto.RepositoryId}\",\n                    MaxCount = 5,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            task {\n                let! parentBranchReferencesResult = Branch.GetReferences(getParentBranchReferencesParameters)\n\n                return\n                    match parentBranchReferencesResult with\n                    | Ok returnValue -> References returnValue.ReturnValue\n                    | Error error -> FetchError error\n            }\n        else\n            Task.FromResult NoParentBranch\n\n    let private renderBranchStatus\n        (parseResult: ParseResult)\n        (branchDto: BranchDto)\n        (parentBranchDto: BranchDto)\n        (mostRecentReferences: ReferenceDto array)\n        (parentBranchReferencesState: ParentBranchReferencesState)\n        =\n        let horizontalLineChar = \"-\"\n        let separator = \"-\"\n\n        // Now that I have the current and parent branch, I can get the details for the latest\n        // promotion, latest commit, latest checkpoint, and latest save.\n        let latestSave = branchDto.LatestSave\n        let latestCheckpoint = branchDto.LatestCheckpoint\n        let latestCommit = branchDto.LatestCommit\n        let latestParentBranchPromotion = parentBranchDto.LatestPromotion\n        let basedOn = branchDto.BasedOn\n\n        let longestAgoLength =\n            [\n                latestSave\n                latestCheckpoint\n                latestCommit\n                latestParentBranchPromotion\n                basedOn\n            ]\n            |> Seq.map (fun b -> (ago b.CreatedAt).Length)\n            |> Seq.max\n\n        let aligned (s: string) =\n            let space = \" \"\n            $\"{String.replicate (longestAgoLength - s.Length) space}{s}\"\n\n        let permissions (branchDto: BranchDto) =\n            let sb = stringBuilderPool.Get()\n\n            try\n                if branchDto.PromotionEnabled then sb.Append(\"Promotion/\") |> ignore\n                if branchDto.CommitEnabled then sb.Append(\"Commit/\") |> ignore\n                if branchDto.CheckpointEnabled then sb.Append(\"Checkpoint/\") |> ignore\n                if branchDto.SaveEnabled then sb.Append(\"Save/\") |> ignore\n                if branchDto.TagEnabled then sb.Append(\"Tag/\") |> ignore\n                if branchDto.ExternalEnabled then sb.Append(\"External/\") |> ignore\n\n                if sb.Length > 0 && sb[sb.Length - 1] = '/' then\n                    sb.Remove(sb.Length - 1, 1) |> ignore\n\n                sb.ToString()\n            finally\n                if not <| isNull sb then stringBuilderPool.Return(sb)\n\n        let ownerLabel = Utilities.getLocalizedString Text.StringResourceName.Owner\n        let organizationLabel = Utilities.getLocalizedString Text.StringResourceName.Organization\n        let repositoryLabel = Utilities.getLocalizedString Text.StringResourceName.Repository\n        let branchLabel = Utilities.getLocalizedString Text.StringResourceName.Branch\n\n        let headerLength =\n            ownerLabel.Length\n            + organizationLabel.Length\n            + repositoryLabel.Length\n            + 6\n\n        let column1 = TableColumn(String.replicate headerLength horizontalLineChar)\n\n        let basedOnMessage =\n            if branchDto.BasedOn.ReferenceId = parentBranchDto.LatestPromotion.ReferenceId\n               || branchDto.ParentBranchId = Constants.DefaultParentBranchId then\n                $\"[{Colors.Added}]Based on latest promotion[/]\"\n            else\n                $\"[{Colors.Important}]Not based on latest promotion[/]\"\n\n        let referenceTable =\n            (createReferenceTable parseResult mostRecentReferences)\n                .Expand()\n\n        let ownerOrgRepoHeader =\n            if parseResult |> verbose then\n                $\"[{Colors.Important}]{ownerLabel}:[/] {Current().OwnerName} [{Colors.Deemphasized}]{separator} {Current().OwnerId}[/] {separator} [{Colors.Important}]{organizationLabel}:[/] {Current().OrganizationName} [{Colors.Deemphasized}]{separator} {Current().OrganizationId}[/] {separator} [{Colors.Important}]{repositoryLabel}:[/] {Current().RepositoryName} [{Colors.Deemphasized}]{separator} {Current().RepositoryId}[/]\"\n            else\n                $\"[{Colors.Important}]{ownerLabel}:[/] {Current().OwnerName} {separator} [{Colors.Important}]{organizationLabel}:[/] {Current().OrganizationName} {separator} [{Colors.Important}]{repositoryLabel}:[/] {Current().RepositoryName}\"\n\n        let branchHeader =\n            if parseResult |> verbose then\n                $\"[{Colors.Important}] {branchLabel}:[/] {branchDto.BranchName} [{Colors.Deemphasized}]{separator}[/] {basedOnMessage} [{Colors.Deemphasized}]{separator} Allows {permissions branchDto} {separator} {branchDto.BranchId} [/]\"\n            else\n                $\"[{Colors.Important}] {branchLabel}:[/] {branchDto.BranchName} [{Colors.Deemphasized}]{separator}[/] {basedOnMessage} [{Colors.Deemphasized}]{separator} Allows {permissions branchDto} [/]\"\n\n        let parentBranchHeader =\n            if parseResult |> verbose then\n                $\"[{Colors.Important}] Parent branch:[/] {parentBranchDto.BranchName} [{Colors.Deemphasized}]{separator} Allows {permissions parentBranchDto} {separator} {parentBranchDto.BranchId} [/]\"\n            else\n                $\"[{Colors.Important}] Parent branch:[/] {parentBranchDto.BranchName} [{Colors.Deemphasized}]{separator} Allows {permissions parentBranchDto} [/]\"\n\n        let commitReferenceTable =\n            let sortedReferences =\n                [| latestCommit; latestCheckpoint |]\n                |> Array.sortByDescending (fun r -> r.CreatedAt)\n\n            (createReferenceTable parseResult sortedReferences)\n                .Expand()\n\n        let outerTable =\n            Table(Border = TableBorder.None, ShowHeaders = false)\n                .AddColumns(column1)\n\n        let branchTable = Table(ShowHeaders = false, Border = TableBorder.None, Expand = true)\n\n        branchTable\n            .AddColumn(column1)\n            .AddEmptyRow()\n            .AddRow($\"[{Colors.Important}] Most recent references:[/]\")\n            .AddRow(Padder(referenceTable).Padding(1, 0, 0, 0))\n            .AddEmptyRow()\n        |> ignore\n\n        let branchPanel = Panel(branchTable, Expand = true)\n        branchPanel.Header <- PanelHeader(branchHeader, Justify.Left)\n        branchPanel.Border <- BoxBorder.Double\n\n        outerTable.AddRow(branchPanel).AddEmptyRow()\n        |> ignore\n\n        match parentBranchReferencesState with\n        | References parentBranchReferences ->\n            let parentBranchReferencesTable =\n                (createReferenceTable parseResult parentBranchReferences)\n                    .Expand()\n\n            let parentBranchTable = Table(ShowHeaders = false, Border = TableBorder.None, Expand = true)\n\n            parentBranchTable\n                .AddColumn(column1)\n                .AddEmptyRow()\n                .AddRow($\"[{Colors.Important}] Most recent references:[/]\")\n                .AddRow(\n                    Padder(parentBranchReferencesTable)\n                        .Padding(1, 0, 0, 0)\n                )\n            |> ignore\n\n            let parentBranchPanel = Panel(parentBranchTable, Expand = true)\n            parentBranchPanel.Header <- PanelHeader(parentBranchHeader, Justify.Left)\n            parentBranchPanel.Border <- BoxBorder.Double\n            outerTable.AddRow(parentBranchPanel) |> ignore\n        | FetchError error -> logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n        | NoParentBranch ->\n            outerTable.AddRow($\"[{Colors.Important}]Parent branch[/]: None\")\n            |> ignore\n\n        outerTable\n            .AddEmptyRow()\n            .AddRow(ownerOrgRepoHeader)\n        |> ignore\n\n        AnsiConsole.Write(outerTable)\n\n        0\n\n    let private statusHandlerImpl (parseResult: ParseResult) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            do! Auth.ensureAccessToken parseResult\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n            // Show repo and branch names.\n            let getParameters =\n                GetBranchParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    BranchId = graceIds.BranchIdString,\n                    BranchName = graceIds.BranchName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            let! branchResult = Branch.Get(getParameters)\n            let! parentBranchResult = Branch.GetParentBranch(getParameters)\n\n            match branchResult, parentBranchResult with\n            | Ok branchReturnValue, Ok parentBranchReturnValue ->\n                let branchDto = branchReturnValue.ReturnValue\n                let parentBranchDto = parentBranchReturnValue.ReturnValue\n\n                let getReferencesParameters =\n                    Parameters.Branch.GetReferencesParameters(\n                        BranchId = graceIds.BranchIdString,\n                        BranchName = graceIds.BranchName,\n                        OwnerId = graceIds.OwnerIdString,\n                        OwnerName = graceIds.OwnerName,\n                        OrganizationId = graceIds.OrganizationIdString,\n                        OrganizationName = graceIds.OrganizationName,\n                        RepositoryId = graceIds.RepositoryIdString,\n                        RepositoryName = graceIds.RepositoryName,\n                        MaxCount = 10,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                let! referencesResult = Branch.GetReferences(getReferencesParameters)\n\n                let mostRecentReferences =\n                    match referencesResult with\n                    | Ok returnValue -> returnValue.ReturnValue\n                    | Error _ -> Array.Empty<ReferenceDto>()\n\n                let! parentBranchReferencesState = getParentBranchReferencesState graceIds branchDto\n\n                return renderBranchStatus parseResult branchDto parentBranchDto mostRecentReferences parentBranchReferencesState\n            | Error error, _\n            | _, Error error ->\n                logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                return -1\n        }\n\n    let private statusHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! statusHandlerImpl parseResult\n            with\n            | :? OperationCanceledException -> return 1\n            | ex ->\n                logToAnsiConsole Colors.Error (Markup.Escape($\"{ExceptionResponse.Create ex}\"))\n                return -1\n        }\n\n    type Status() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                return! statusHandler parseResult\n            }\n\n    let private deleteHandlerImpl (parseResult: ParseResult) =\n        if parseResult |> verbose then printParseResult parseResult\n\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n        let validateIncomingParameters = parseResult |> CommonValidations\n\n        match validateIncomingParameters with\n        | Error error -> Task.FromResult(Error error)\n        | Ok _ ->\n            let force = parseResult.GetValue(Options.force)\n            let reassignChildBranches = parseResult.GetValue(Options.reassignChildBranches)\n\n            let newParentBranchId =\n                parseResult.GetValue(Options.newParentBranchId)\n                |> valueOrEmpty\n\n            let newParentBranchName =\n                parseResult.GetValue(Options.newParentBranchName)\n                |> valueOrEmpty\n\n            // Validate that --force and --reassign-child-branches are not both specified\n            if force && reassignChildBranches then\n                Task.FromResult(\n                    Error(GraceError.Create (BranchError.getErrorMessage BranchError.CannotSpecifyBothForceAndReassignChildBranches) graceIds.CorrelationId)\n                )\n            else\n                let deleteParameters =\n                    Parameters.Branch.DeleteBranchParameters(\n                        BranchId = graceIds.BranchIdString,\n                        BranchName = graceIds.BranchName,\n                        OwnerId = graceIds.OwnerIdString,\n                        OwnerName = graceIds.OwnerName,\n                        OrganizationId = graceIds.OrganizationIdString,\n                        OrganizationName = graceIds.OrganizationName,\n                        RepositoryId = graceIds.RepositoryIdString,\n                        RepositoryName = graceIds.RepositoryName,\n                        Force = force,\n                        ReassignChildBranches = reassignChildBranches,\n                        NewParentBranchId = newParentBranchId,\n                        NewParentBranchName = newParentBranchName,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                if parseResult |> hasOutput then\n                    progress\n                        .Columns(progressColumns)\n                        .StartAsync(fun progressContext ->\n                            task {\n                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                let! result = Branch.Delete(deleteParameters)\n                                t0.Increment(100.0)\n                                return result\n                            })\n                else\n                    Branch.Delete(deleteParameters)\n\n    let private deleteHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! deleteHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    type Delete() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let! result = deleteHandler parseResult\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    let private updateParentBranchHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let validateIncomingParameters = parseResult |> CommonValidations\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    let newParentBranchId =\n                        parseResult.GetValue(Options.newParentBranchId)\n                        |> valueOrEmpty\n\n                    let newParentBranchName =\n                        parseResult.GetValue(Options.newParentBranchName)\n                        |> valueOrEmpty\n\n                    let updateParentBranchParameters =\n                        Parameters.Branch.UpdateParentBranchParameters(\n                            BranchId = graceIds.BranchIdString,\n                            BranchName = graceIds.BranchName,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            NewParentBranchId = newParentBranchId,\n                            NewParentBranchName = newParentBranchName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    if parseResult |> hasOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                        let! result = Branch.UpdateParentBranch(updateParentBranchParameters)\n                                        t0.Increment(100.0)\n                                        return result\n                                    })\n                    else\n                        return! Branch.UpdateParentBranch(updateParentBranchParameters)\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    type UpdateParentBranch() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let! result = updateParentBranchHandler parseResult\n                    return result |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    //type UndeleteParameters() =\n    //    inherit CommonParameters()\n    //let private undeleteHandler (parseResult: ParseResult) (undeleteParameters: UndeleteParameters) =\n    //    task {\n    //        try\n    //            if parseResult |> verbose then printParseResult parseResult\n    //            let validateIncomingParameters = parseResult |> CommonValidations\n    //            match validateIncomingParameters with\n    //            | Ok _ ->\n    //                let parameters = Parameters.Owner.UndeleteParameters(OwnerId = undeletecontext.OwnerId, OwnerName = undeletecontext.OwnerName, CorrelationId = undeletecontext.CorrelationId)\n    //                if parseResult |> showOutput then\n    //                    return! progress.Columns(progressColumns)\n    //                            .StartAsync(fun progressContext ->\n    //                            task {\n    //                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n    //                                let! result = Owner.Undelete(parameters)\n    //                                t0.Increment(100.0)\n    //                                return result\n    //                            })\n    //                else\n    //                    return! Owner.Undelete(parameters)\n    //            | Error error -> return Error error\n    //        with\n    //            | ex -> return Error (GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n    //    }\n    //let private Undelete =\n    //    CommandHandler.Create(fun (parseResult: ParseResult) (undeleteParameters: UndeleteParameters) ->\n    //        task {\n    //            let! result = undeleteHandler parseResult undeleteParameters\n    //            return result |> renderOutput parseResult\n    //        })\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n            |> addOption Options.branchName\n            |> addOption Options.branchId\n\n        // Create main command and aliases, if any.`\n        let branchCommand = new Command(\"branch\", Description = \"Create, change, or delete branch-level information.\")\n\n        branchCommand.Aliases.Add(\"br\")\n\n        // Add subcommands.\n        let branchCreateCommand =\n            new Command(\"create\", Description = \"Create a new branch.\")\n            |> addOption Options.branchNameRequired\n            |> addOption Options.branchId\n            |> addOption Options.parentBranchName\n            |> addOption Options.parentBranchId\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n            |> addOption Options.initialPermissions\n            |> addOption Options.doNotSwitch\n\n        branchCreateCommand.Action <- new Create()\n        branchCommand.Subcommands.Add(branchCreateCommand)\n\n        let switchCommand =\n            new Command(\n                \"switch\",\n                Description =\n                    \"Switches your current branch to another branch, or to a specific reference or Sha256Hash. If a Sha256Hash is provided, the current branch will be set to the version with that hash.\"\n            )\n            |> addOption Options.toBranchId\n            |> addOption Options.toBranchName\n            |> addOption Options.sha256Hash\n            |> addOption Options.referenceId\n            |> addCommonOptions\n\n        switchCommand.Aliases.Add(\"download\")\n        switchCommand.Action <- new Switch()\n        branchCommand.Subcommands.Add(switchCommand)\n\n        let statusCommand =\n            new Command(\"status\", Description = \"Displays status information about the current repository and branch.\")\n            |> addCommonOptions\n\n        statusCommand.Action <- new Status()\n        branchCommand.Subcommands.Add(statusCommand)\n\n        let promoteCommand =\n            new Command(\"promote\", Description = \"Promotes a commit into the parent branch.\")\n            |> addOption Options.message\n            |> addOption Options.individual\n            |> addCommonOptions\n\n        promoteCommand.Action <- new Promote()\n        branchCommand.Subcommands.Add(promoteCommand)\n\n        let commitCommand =\n            new Command(\"commit\", Description = \"Create a commit.\")\n            |> addOption Options.messageRequired\n            |> addCommonOptions\n\n        commitCommand.Action <- new Commit()\n        branchCommand.Subcommands.Add(commitCommand)\n\n        let checkpointCommand =\n            new Command(\"checkpoint\", Description = \"Create a checkpoint.\")\n            |> addOption Options.message\n            |> addCommonOptions\n\n        checkpointCommand.Action <- new Checkpoint()\n        branchCommand.Subcommands.Add(checkpointCommand)\n\n        let saveCommand =\n            new Command(\"save\", Description = \"Create a save.\")\n            |> addOption Options.message\n            |> addCommonOptions\n\n        saveCommand.Action <- new Save()\n        branchCommand.Subcommands.Add(saveCommand)\n\n        let tagCommand =\n            new Command(\"tag\", Description = \"Create a tag.\")\n            |> addOption Options.messageRequired\n            |> addCommonOptions\n\n        tagCommand.Action <- new Tag()\n        branchCommand.Subcommands.Add(tagCommand)\n\n        let createExternalCommand =\n            new Command(\"create-external\", Description = \"Create an external reference.\")\n            |> addOption Options.messageRequired\n            |> addCommonOptions\n\n        createExternalCommand.Action <- new CreateExternal()\n        branchCommand.Subcommands.Add(createExternalCommand)\n\n        let rebaseCommand =\n            new Command(\"rebase\", Description = \"Rebase this branch on a promotion from the parent branch.\")\n            |> addCommonOptions\n\n        rebaseCommand.Action <- new Rebase()\n        branchCommand.Subcommands.Add(rebaseCommand)\n\n        let listContentsCommand =\n            new Command(\"list-contents\", Description = \"List directories and files in the current branch.\")\n            |> addOption Options.referenceId\n            |> addOption Options.sha256Hash\n            |> addOption Options.forceRecompute\n            |> addCommonOptions\n\n        listContentsCommand.Action <- new ListContents()\n        branchCommand.Subcommands.Add(listContentsCommand)\n\n        let getRecursiveSizeCommand =\n            new Command(\"get-recursive-size\", Description = \"Get the recursive size of the current branch.\")\n            |> addOption Options.referenceId\n            |> addOption Options.sha256Hash\n            |> addCommonOptions\n\n        getRecursiveSizeCommand.Action <- new GetRecursiveSize()\n        branchCommand.Subcommands.Add(getRecursiveSizeCommand)\n\n        let enableAssignCommand =\n            new Command(\"enable-assign\", Description = \"Enable or disable assigning promotions on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableAssignCommand.Action <- new EnableAssign()\n        branchCommand.Subcommands.Add(enableAssignCommand)\n\n        let enablePromotionCommand =\n            new Command(\"enable-promotion\", Description = \"Enable or disable promotions on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enablePromotionCommand.Action <- new EnablePromotion()\n        branchCommand.Subcommands.Add(enablePromotionCommand)\n\n        let enableCommitCommand =\n            new Command(\"enable-commit\", Description = \"Enable or disable commits on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableCommitCommand.Action <- new EnableCommit()\n        branchCommand.Subcommands.Add(enableCommitCommand)\n\n        let enableCheckpointsCommand =\n            new Command(\"enable-checkpoints\", Description = \"Enable or disable checkpoints on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableCheckpointsCommand.Action <- new EnableCheckpoint()\n        branchCommand.Subcommands.Add(enableCheckpointsCommand)\n\n        let enableSaveCommand =\n            new Command(\"enable-save\", Description = \"Enable or disable saves on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableSaveCommand.Action <- new EnableSave()\n        branchCommand.Subcommands.Add(enableSaveCommand)\n\n        let enableTagCommand =\n            new Command(\"enable-tag\", Description = \"Enable or disable tags on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableTagCommand.Action <- new EnableTag()\n        branchCommand.Subcommands.Add(enableTagCommand)\n\n        let enableExternalCommand =\n            new Command(\"enable-external\", Description = \"Enable or disable external references on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableExternalCommand.Action <- new EnableExternal()\n        branchCommand.Subcommands.Add(enableExternalCommand)\n\n        let enableAutoRebaseCommand =\n            new Command(\"enable-auto-rebase\", Description = \"Enable or disable auto-rebase on this branch.\")\n            |> addOption Options.enabled\n            |> addCommonOptions\n\n        enableAutoRebaseCommand.Action <- new EnableAutoRebase()\n        branchCommand.Subcommands.Add(enableAutoRebaseCommand)\n\n        let setPromotionModeCommand =\n            new Command(\"set-promotion-mode\", Description = \"Set the promotion mode for the branch (IndividualOnly, GroupOnly, or Hybrid).\")\n            |> addOption Options.promotionMode\n            |> addCommonOptions\n\n        setPromotionModeCommand.Action <- new SetPromotionMode()\n        branchCommand.Subcommands.Add(setPromotionModeCommand)\n\n        let setNameCommand =\n            new Command(\"set-name\", Description = \"Change the name of the branch.\")\n            |> addOption Options.newName\n            |> addCommonOptions\n\n        setNameCommand.Action <- new SetName()\n        branchCommand.Subcommands.Add(setNameCommand)\n\n        let getCommand =\n            new Command(\"get\", Description = \"Gets details for the branch.\")\n            |> addOption Options.includeDeleted\n            |> addOption Options.showEvents\n            |> addCommonOptions\n\n        getCommand.Action <- new Get()\n        branchCommand.Subcommands.Add(getCommand)\n\n        let getReferencesCommand =\n            new Command(\"get-references\", Description = \"Retrieves a list of the most recent references from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getReferencesCommand.Action <- new GetReferences()\n        branchCommand.Subcommands.Add(getReferencesCommand)\n\n        let getPromotionsCommand =\n            new Command(\"get-promotions\", Description = \"Retrieves a list of the most recent promotions from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getPromotionsCommand.Action <- new GetPromotions()\n        branchCommand.Subcommands.Add(getPromotionsCommand)\n\n        let getCommitsCommand =\n            new Command(\"get-commits\", Description = \"Retrieves a list of the most recent commits from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getCommitsCommand.Action <- new GetCommits()\n        branchCommand.Subcommands.Add(getCommitsCommand)\n\n        let getCheckpointsCommand =\n            new Command(\"get-checkpoints\", Description = \"Retrieves a list of the most recent checkpoints from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getCheckpointsCommand.Action <- new GetCheckpoints()\n        branchCommand.Subcommands.Add(getCheckpointsCommand)\n\n        let getSavesCommand =\n            new Command(\"get-saves\", Description = \"Retrieves a list of the most recent saves from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getSavesCommand.Action <- new GetSaves()\n        branchCommand.Subcommands.Add(getSavesCommand)\n\n        let getTagsCommand =\n            new Command(\"get-tags\", Description = \"Retrieves a list of the most recent tags from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getTagsCommand.Action <- new GetTags()\n        branchCommand.Subcommands.Add(getTagsCommand)\n\n        let getExternalsCommand =\n            new Command(\"get-externals\", Description = \"Retrieves a list of the most recent external references from the branch.\")\n            |> addCommonOptions\n            |> addOption Options.maxCount\n            |> addOption Options.fullSha\n\n        getExternalsCommand.Action <- new GetExternals()\n        branchCommand.Subcommands.Add(getExternalsCommand)\n\n        let deleteCommand =\n            new Command(\"delete\", Description = \"Delete the branch.\")\n            |> addOption Options.force\n            |> addOption Options.reassignChildBranches\n            |> addOption Options.newParentBranchId\n            |> addOption Options.newParentBranchName\n            |> addCommonOptions\n\n        deleteCommand.Action <- new Delete()\n        branchCommand.Subcommands.Add(deleteCommand)\n\n        let updateParentBranchCommand =\n            new Command(\"update-parent-branch\", Description = \"Update the parent branch of this branch.\")\n            |> addOption Options.newParentBranchId\n            |> addOption Options.newParentBranchName\n            |> addCommonOptions\n\n        updateParentBranchCommand.Action <- new UpdateParentBranch()\n        branchCommand.Subcommands.Add(updateParentBranchCommand)\n\n        let assignCommand =\n            new Command(\"assign\", Description = \"Assign a promotion to this branch.\")\n            |> addOption Options.directoryVersionId\n            |> addOption Options.sha256Hash\n            |> addOption Options.message\n            |> addCommonOptions\n\n        assignCommand.Action <- new Assign()\n        branchCommand.Subcommands.Add(assignCommand)\n\n        //let undeleteCommand = new Command(\"undelete\", Description = \"Undelete a deleted owner.\") |> addCommonOptions\n        //undeleteCommand.Action <- Undelete\n        //branchCommand.Subcommands.Add(undeleteCommand)\n\n        branchCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Candidate.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Spectre.Console\nopen System\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule CandidateCommand =\n    module private Options =\n        let candidateId =\n            new Option<string>(\n                \"--candidate\",\n                [| \"--candidate-id\" |],\n                Required = true,\n                Description = \"The candidate ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let gate = new Option<string>(\"--gate\", Required = true, Description = \"The gate name to rerun.\", Arity = ArgumentArity.ExactlyOne)\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The organization's name. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                Required = false,\n                Description = \"The repository's name. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    let internal tryParseCandidateId (candidateId: string) (parseResult: ParseResult) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(candidateId)\n           || not (Guid.TryParse(candidateId, &parsed))\n           || parsed = Guid.Empty then\n            Error(GraceError.Create \"CandidateId must be a valid non-empty Guid.\" (getCorrelationId parseResult))\n        else\n            Ok(parsed.ToString())\n\n    let internal buildCandidateProjectionParameters (graceIds: GraceIds) (candidateId: string) =\n        Parameters.Review.CandidateProjectionParameters(\n            CandidateId = candidateId,\n            OwnerId = graceIds.OwnerIdString,\n            OwnerName = graceIds.OwnerName,\n            OrganizationId = graceIds.OrganizationIdString,\n            OrganizationName = graceIds.OrganizationName,\n            RepositoryId = graceIds.RepositoryIdString,\n            RepositoryName = graceIds.RepositoryName,\n            CorrelationId = graceIds.CorrelationId\n        )\n\n    let internal buildCandidateGateRerunParameters (graceIds: GraceIds) (candidateId: string) (gate: string) =\n        Parameters.Review.CandidateGateRerunParameters(\n            CandidateId = candidateId,\n            Gate = gate,\n            OwnerId = graceIds.OwnerIdString,\n            OwnerName = graceIds.OwnerName,\n            OrganizationId = graceIds.OrganizationIdString,\n            OrganizationName = graceIds.OrganizationName,\n            RepositoryId = graceIds.RepositoryIdString,\n            RepositoryName = graceIds.RepositoryName,\n            CorrelationId = graceIds.CorrelationId\n        )\n\n    let private renderCandidateSnapshot (parseResult: ParseResult) (snapshot: Parameters.Review.CandidateProjectionSnapshotResult) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let table = Table(Border = TableBorder.Rounded)\n            table.AddColumn(\"Field\") |> ignore\n            table.AddColumn(\"Value\") |> ignore\n\n            table.AddRow(\"Candidate\", Markup.Escape(snapshot.Identity.CandidateId))\n            |> ignore\n\n            table.AddRow(\"PromotionSet\", Markup.Escape(snapshot.Identity.PromotionSetId))\n            |> ignore\n\n            table.AddRow(\"PromotionSetStatus\", Markup.Escape(snapshot.PromotionSetStatus))\n            |> ignore\n\n            table.AddRow(\"StepsComputationStatus\", Markup.Escape(snapshot.StepsComputationStatus))\n            |> ignore\n\n            table.AddRow(\"QueueState\", Markup.Escape(snapshot.QueueState))\n            |> ignore\n\n            table.AddRow(\n                \"RunningPromotionSetId\",\n                if String.IsNullOrWhiteSpace(snapshot.RunningPromotionSetId) then\n                    \"-\"\n                else\n                    Markup.Escape(snapshot.RunningPromotionSetId)\n            )\n            |> ignore\n\n            table.AddRow(\"UnresolvedFindings\", snapshot.UnresolvedFindingCount.ToString())\n            |> ignore\n\n            table.AddRow(\"ValidationSummaryAvailable\", snapshot.ValidationSummaryAvailable.ToString())\n            |> ignore\n\n            table.AddRow(\"RequiredActions\", Markup.Escape(String.Join(\", \", snapshot.RequiredActions)))\n            |> ignore\n\n            if not snapshot.Diagnostics.IsEmpty then\n                table.AddRow(\"Diagnostics\", Markup.Escape(String.Join(\" | \", snapshot.Diagnostics)))\n                |> ignore\n\n            AnsiConsole.Write(table)\n\n    let private renderCandidateRequiredActions (parseResult: ParseResult) (result: Parameters.Review.CandidateRequiredActionsResult) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            AnsiConsole.MarkupLine($\"[bold]Candidate[/] {Markup.Escape(result.Identity.CandidateId)}\")\n            let requiredActionsText = String.Join(\", \", result.RequiredActions)\n            AnsiConsole.MarkupLine($\"[bold]Required actions:[/] {Markup.Escape(requiredActionsText)}\")\n\n            if not result.Diagnostics.IsEmpty then\n                let diagnosticsText = String.Join(\" | \", result.Diagnostics)\n                AnsiConsole.MarkupLine($\"[yellow]Diagnostics:[/] {Markup.Escape(diagnosticsText)}\")\n\n    let private renderCandidateAttestations (parseResult: ParseResult) (result: Parameters.Review.CandidateAttestationsResult) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let table = Table(Border = TableBorder.Rounded)\n            table.AddColumn(\"Attestation\") |> ignore\n            table.AddColumn(\"Status\") |> ignore\n            table.AddColumn(\"Detail\") |> ignore\n\n            result.Attestations\n            |> List.iter (fun attestation ->\n                table.AddRow(Markup.Escape(attestation.Name), Markup.Escape(attestation.Status), Markup.Escape(attestation.Detail))\n                |> ignore)\n\n            AnsiConsole.Write(table)\n\n            if not result.Diagnostics.IsEmpty then\n                let diagnosticsText = String.Join(\" | \", result.Diagnostics)\n                AnsiConsole.MarkupLine($\"[yellow]Diagnostics:[/] {Markup.Escape(diagnosticsText)}\")\n\n    let private renderCandidateActionResult (parseResult: ParseResult) (result: Parameters.Review.CandidateActionResult) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            AnsiConsole.MarkupLine(\n                $\"[green]Candidate action[/] {Markup.Escape(result.Action)} [green]completed for[/] {Markup.Escape(result.Identity.CandidateId)}\"\n            )\n\n            if not result.AppliedOperations.IsEmpty then\n                let operationsText = String.Join(\" -> \", result.AppliedOperations)\n                AnsiConsole.MarkupLine($\"[bold]Operations:[/] {Markup.Escape(operationsText)}\")\n\n            if not result.Diagnostics.IsEmpty then\n                let diagnosticsText = String.Join(\" | \", result.Diagnostics)\n                AnsiConsole.MarkupLine($\"[yellow]Diagnostics:[/] {Markup.Escape(diagnosticsText)}\")\n\n    let private resolveCandidateFromParseResult (parseResult: ParseResult) =\n        let candidateId = parseResult.GetValue(Options.candidateId)\n        tryParseCandidateId candidateId parseResult\n\n    let private getHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match resolveCandidateFromParseResult parseResult with\n                | Error error -> return Error error\n                | Ok candidateId ->\n                    let parameters = buildCandidateProjectionParameters graceIds candidateId\n                    let! result = Review.GetCandidate(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        renderCandidateSnapshot parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Get() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = getHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private requiredActionsHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match resolveCandidateFromParseResult parseResult with\n                | Error error -> return Error error\n                | Ok candidateId ->\n                    let parameters = buildCandidateProjectionParameters graceIds candidateId\n                    let! result = Review.GetCandidateRequiredActions(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        renderCandidateRequiredActions parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type RequiredActions() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = requiredActionsHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private attestationsHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match resolveCandidateFromParseResult parseResult with\n                | Error error -> return Error error\n                | Ok candidateId ->\n                    let parameters = buildCandidateProjectionParameters graceIds candidateId\n                    let! result = Review.GetCandidateAttestations(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        renderCandidateAttestations parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Attestations() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = attestationsHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private retryHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match resolveCandidateFromParseResult parseResult with\n                | Error error -> return Error error\n                | Ok candidateId ->\n                    let parameters = buildCandidateProjectionParameters graceIds candidateId\n                    let! result = Review.RetryCandidate(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        renderCandidateActionResult parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Retry() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = retryHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private cancelHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match resolveCandidateFromParseResult parseResult with\n                | Error error -> return Error error\n                | Ok candidateId ->\n                    let parameters = buildCandidateProjectionParameters graceIds candidateId\n                    let! result = Review.CancelCandidate(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        renderCandidateActionResult parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Cancel() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = cancelHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private gateRerunHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let gate =\n                    parseResult.GetValue(Options.gate)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                if String.IsNullOrWhiteSpace gate then\n                    return Error(GraceError.Create \"Gate is required for candidate gate rerun.\" (getCorrelationId parseResult))\n                else\n                    match resolveCandidateFromParseResult parseResult with\n                    | Error error -> return Error error\n                    | Ok candidateId ->\n                        let parameters = buildCandidateGateRerunParameters graceIds candidateId gate\n                        let! result = Review.RerunCandidateGate(parameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            renderCandidateActionResult parseResult returnValue.ReturnValue\n                            return Ok returnValue\n                        | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type GateRerun() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = gateRerunHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let candidateCommand =\n            new Command(\"candidate\", Description = \"Candidate-first reviewer operations projected over PromotionSet-backed runtime semantics.\")\n\n        let getCommand =\n            new Command(\"get\", Description = \"Get candidate projection details.\")\n            |> addOption Options.candidateId\n            |> addCommonOptions\n\n        getCommand.Action <- new Get()\n        candidateCommand.Subcommands.Add(getCommand)\n\n        let requiredActionsCommand =\n            new Command(\"required-actions\", Description = \"Get deterministic required actions for a candidate.\")\n            |> addOption Options.candidateId\n            |> addCommonOptions\n\n        requiredActionsCommand.Action <- new RequiredActions()\n        candidateCommand.Subcommands.Add(requiredActionsCommand)\n\n        let attestationsCommand =\n            new Command(\"attestations\", Description = \"Get candidate attestation state from policy and review checkpoints.\")\n            |> addOption Options.candidateId\n            |> addCommonOptions\n\n        attestationsCommand.Action <- new Attestations()\n        candidateCommand.Subcommands.Add(attestationsCommand)\n\n        let retryCommand =\n            new Command(\"retry\", Description = \"Retry candidate processing through recompute and queue operations.\")\n            |> addOption Options.candidateId\n            |> addCommonOptions\n\n        retryCommand.Action <- new Retry()\n        candidateCommand.Subcommands.Add(retryCommand)\n\n        let cancelCommand =\n            new Command(\"cancel\", Description = \"Cancel queued candidate processing.\")\n            |> addOption Options.candidateId\n            |> addCommonOptions\n\n        cancelCommand.Action <- new Cancel()\n        candidateCommand.Subcommands.Add(cancelCommand)\n\n        let gateCommand = new Command(\"gate\", Description = \"Gate-related candidate operations.\")\n\n        let gateRerunCommand =\n            new Command(\"rerun\", Description = \"Rerun candidate gate evaluation semantics.\")\n            |> addOption Options.candidateId\n            |> addOption Options.gate\n            |> addCommonOptions\n\n        gateRerunCommand.Action <- new GateRerun()\n        gateCommand.Subcommands.Add(gateRerunCommand)\n        candidateCommand.Subcommands.Add(gateCommand)\n\n        candidateCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Common.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen FSharpPlus\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.Shared\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Resources.Text\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Spectre.Console\nopen System\nopen System.CommandLine\nopen System.CommandLine.Parsing\nopen System.Globalization\nopen System.Linq\nopen System.Text.Json\nopen System.Threading.Tasks\nopen Spectre.Console.Rendering\nopen Spectre.Console.Json\nopen System.Text.RegularExpressions\n\nmodule Common =\n\n    type ParameterBase() =\n        member val public CorrelationId: string = String.Empty with get, set\n        member val public Json: bool = false with get, set\n        member val public OutputFormat: string = String.Empty with get, set\n\n    /// The output format for the command.\n    type OutputFormat =\n        | Normal\n        | Json\n        | Minimal\n        | Silent\n        | Verbose\n\n    /// Adds an option (i.e. parameter) to a command, so you can do cool stuff like `|> addOption Options.someOption |> addOption Options.anotherOption`.\n    let addOption (option: Option) (command: Command) =\n        command.Options.Add(option)\n        command\n\n    let public Language = CultureInfo.CurrentCulture.TwoLetterISOLanguageName\n\n    /// Gets the \"... ago\" text.\n    let ago = ago Language\n\n    module Options =\n        let correlationId =\n            new Option<String>(\n                OptionName.CorrelationId,\n                [| \"-c\" |],\n                Required = false,\n                Description = \"CorrelationId for end-to-end tracking <String>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                Recursive = true,\n                DefaultValueFactory = (fun _ -> CorrelationId.Empty)\n            )\n\n        let source =\n            new Option<string>(\n                OptionName.Source,\n                Required = false,\n                Description = \"Optional invocation source metadata for history attribution.\",\n                Arity = ArgumentArity.ExactlyOne,\n                Recursive = true\n            )\n\n        let output =\n            (new Option<String>(\n                OptionName.Output,\n                [| \"-o\" |],\n                Required = false,\n                Description = \"The style of output.\",\n                Arity = ArgumentArity.ExactlyOne,\n                Recursive = true,\n                DefaultValueFactory = (fun _ -> \"Normal\")\n            ))\n                .AcceptOnlyFromAmong(listCases<OutputFormat> ())\n\n    /// Gets the correlationId value from the command's ParseResult.\n    let getCorrelationId (parseResult: ParseResult) = Services.resolveCorrelationId parseResult\n\n    [<Literal>]\n    let SourceEnvironmentVariableName = \"GRACE_SOURCE\"\n\n    let private normalizeSource (value: string) = if String.IsNullOrWhiteSpace(value) then None else Some(value.Trim())\n\n    let private tryGetExplicitSourceFromParseResult (parseResult: ParseResult) =\n        if isNull parseResult then\n            None\n        else\n            let result = parseResult.GetResult(OptionName.Source)\n\n            if isNull result then\n                None\n            else\n                try\n                    let optionResult = result :?> OptionResult\n\n                    if optionResult.Implicit then\n                        None\n                    else\n                        parseResult.GetValue<string>(OptionName.Source)\n                        |> normalizeSource\n                with\n                | :? InvalidOperationException -> None\n\n    let resolveInvocationSource (parseResult: ParseResult) =\n        match tryGetExplicitSourceFromParseResult parseResult with\n        | Some source -> Some source\n        | None ->\n            Environment.GetEnvironmentVariable(SourceEnvironmentVariableName)\n            |> normalizeSource\n\n    module Validations =\n        /// Checks that a given name option is a valid Grace name. If the option is not present, it does not return an error.\n        let mustBeAValidGraceName<'T when 'T :> IErrorDiscriminatedUnion> (parseResult: ParseResult) (optionName: string) (error: 'T) =\n            let result = parseResult.GetResult(optionName)\n            let value = parseResult.GetValue<string>(optionName)\n\n            if result <> null\n               && not <| Constants.GraceNameRegex.IsMatch(value) then\n                Error(GraceError.Create (getErrorMessage error) (parseResult |> getCorrelationId))\n            else\n                Ok(parseResult)\n\n        let ``Option must be present`` (optionName: string) (error: IErrorDiscriminatedUnion) (parseResult: ParseResult) =\n            let result = parseResult.GetResult(optionName)\n\n            if isNull result then\n                Error(GraceError.Create (getErrorMessage error) (parseResult |> getCorrelationId))\n            else\n                Ok(parseResult)\n\n        let ``OwnerName must be a valid Grace name`` (parseResult: ParseResult) =\n            mustBeAValidGraceName parseResult OptionName.OwnerName OwnerError.InvalidOwnerName\n\n        let ``OrganizationName must be a valid Grace name`` (parseResult: ParseResult) =\n            mustBeAValidGraceName parseResult OptionName.OrganizationName OrganizationError.InvalidOrganizationName\n\n        let ``RepositoryName must be a valid Grace name`` (parseResult: ParseResult) =\n            mustBeAValidGraceName parseResult OptionName.RepositoryName RepositoryError.InvalidRepositoryName\n\n        let ``BranchName must be a valid Grace name`` (parseResult: ParseResult) =\n            mustBeAValidGraceName parseResult OptionName.BranchName BranchError.InvalidBranchName\n\n        let ``NewName must be a valid Grace name`` (parseResult: ParseResult) =\n            mustBeAValidGraceName parseResult OptionName.NewName RepositoryError.InvalidNewName\n\n        let ``Either OwnerId or OwnerName must be provided`` (parseResult: ParseResult) =\n            // Get the command that was invoked.\n            let command = parseResult.CommandResult.Command\n\n            // Only perform this validation if the command has an OwnerId option.\n            if command.Options.Any(fun option -> option.Name = OptionName.OwnerId) then\n                let ownerIdResult = parseResult.GetResult(OptionName.OwnerId) :?> OptionResult\n                let ownerId = parseResult.GetValue<Guid>(OptionName.OwnerId)\n                let ownerName = parseResult.GetValue<string>(OptionName.OwnerName)\n\n                let isOk =\n                    ownerIdResult.Implicit\n                    || ownerId <> Guid.Empty\n                    || not <| String.IsNullOrWhiteSpace(ownerName)\n\n                if isOk then\n                    Ok(parseResult)\n                else\n                    Error(GraceError.Create (getErrorMessage OwnerError.EitherOwnerIdOrOwnerNameRequired) (parseResult |> getCorrelationId))\n            else\n                Ok(parseResult)\n\n        let ``Either OrganizationId or OrganizationName must be provided`` (parseResult: ParseResult) =\n            // Get the command that was invoked.\n            let command = parseResult.CommandResult.Command\n            // Only perform this validation if the command has an OrganizationId option.\n            if command.Options.Any(fun option -> option.Name = OptionName.OrganizationId) then\n                let organizationId = parseResult.GetValue<Guid>(OptionName.OrganizationId)\n                let organizationName = parseResult.GetValue<string>(OptionName.OrganizationName)\n\n                if\n                    organizationId = Guid.Empty\n                    && String.IsNullOrWhiteSpace(organizationName)\n                then\n                    Error(\n                        GraceError.Create (getErrorMessage OrganizationError.EitherOrganizationIdOrOrganizationNameRequired) (parseResult |> getCorrelationId)\n                    )\n                else\n                    Ok(parseResult)\n            else\n                Ok(parseResult)\n\n\n        let ``Either RepositoryId or RepositoryName must be provided`` (parseResult: ParseResult) =\n            // Get the command that was invoked.\n            let command = parseResult.CommandResult.Command\n            // Only perform this validation if the command has a RepositoryId option.\n            if command.Options.Any(fun option -> option.Name = OptionName.RepositoryId) then\n                let repositoryId = parseResult.GetValue<Guid>(OptionName.RepositoryId)\n                let repositoryName = parseResult.GetValue<string>(OptionName.RepositoryName)\n\n                if\n                    repositoryId = Guid.Empty\n                    && String.IsNullOrWhiteSpace(repositoryName)\n                then\n                    Error(GraceError.Create (getErrorMessage RepositoryError.EitherRepositoryIdOrRepositoryNameRequired) (parseResult |> getCorrelationId))\n                else\n                    Ok(parseResult)\n            else\n                Ok(parseResult)\n\n        let ``Either BranchId or BranchName must be provided`` (parseResult: ParseResult) =\n            // Get the command that was invoked.\n            let command = parseResult.CommandResult.Command\n            // Only perform this validation if the command has a BranchId option.\n            if command.Options.Any(fun option -> option.Name = OptionName.BranchId) then\n                let branchId = parseResult.GetValue<Guid>(OptionName.BranchId)\n                let branchName = parseResult.GetValue<string>(OptionName.BranchName)\n\n                if\n                    branchId = Guid.Empty\n                    && String.IsNullOrWhiteSpace(branchName)\n                then\n                    Error(GraceError.Create (getErrorMessage BranchError.EitherBranchIdOrBranchNameRequired) (parseResult |> getCorrelationId))\n                else\n                    Ok(parseResult)\n            else\n                Ok(parseResult)\n\n        let CommonValidations (parseResult: ParseResult) =\n            parseResult\n            |> ``OwnerName must be a valid Grace name``\n            >>= ``OrganizationName must be a valid Grace name``\n            >>= ``RepositoryName must be a valid Grace name``\n            >>= ``BranchName must be a valid Grace name``\n            >>= ``NewName must be a valid Grace name``\n\n    //>>= ``Either OwnerId or OwnerName must be provided``\n    //>>= ``Either OrganizationId or OrganizationName must be provided``\n    //>>= ``Either RepositoryId or RepositoryName must be provided``\n    //>>= ``Either BranchId or BranchName must be provided``\n\n    /// Checks if the output format from the command line is a specific format.\n    let isOutputFormat (outputFormat: OutputFormat) (parseResult: ParseResult) =\n        try\n            let outputOption = parseResult.GetValue(Options.output)\n\n            match outputOption with\n            | null ->\n                // The command didn't have an output option set, which means it defaults to Normal.\n                if outputFormat = OutputFormat.Normal then true else false\n            | _ ->\n                // The command had an output option set, so we check if it matches the expected output format.\n                let formatFromCommand = parseResult.GetValue<string>(Options.output)\n\n                if outputFormat = discriminatedUnionFromString<OutputFormat>(\n                    formatFromCommand\n                )\n                    .Value then\n                    true\n                else\n                    false\n        with\n        | ex ->\n            logToAnsiConsole Colors.Error $\"Exception in isOutputFormat: {ExceptionResponse.Create ex}\"\n            false\n\n    /// Checks if the output format from the command line is Json.\n    let json parseResult = parseResult |> isOutputFormat Json\n\n    /// Checks if the output format from the command line is Minimal.\n    let minimal parseResult = parseResult |> isOutputFormat Minimal\n\n    /// Checks if the output format from the command line is Normal.\n    let normal parseResult = parseResult |> isOutputFormat Normal\n\n    /// Checks if the output format from the command line is Silent.\n    let silent parseResult = parseResult |> isOutputFormat Silent\n\n    /// Checks if the output format from the command line is Verbose.\n    let verbose parseResult = parseResult |> isOutputFormat Verbose\n\n    /// Checks if the output format from the command line is either Normal or Verbose; i.e. it has output.\n    let hasOutput parseResult = parseResult |> normal || parseResult |> verbose\n\n    let startProgressTask showOutput (t: ProgressTask) = if showOutput then t.StartTask()\n\n    let setProgressTaskValue showOutput (value: float) (t: ProgressTask) = if showOutput then t.Value <- value\n\n    let incrementProgressTaskValue showOutput (value: float) (t: ProgressTask) = if showOutput then t.Increment(value)\n\n    let emptyTask = ProgressTask(0, \"Empty progress task\", 0.0, autoStart = false)\n\n    /// Rewrites \"[\" to \"[[\" and \"]\" to \"]]\".\n    let escapeBrackets s = s.ToString().Replace(\"[\", \"[[\").Replace(\"]\", \"]]\")\n\n    let private resolvedValueOptionNames =\n        [\n            OptionName.OwnerId\n            OptionName.OwnerName\n            OptionName.OrganizationId\n            OptionName.OrganizationName\n            OptionName.RepositoryId\n            OptionName.RepositoryName\n            OptionName.BranchId\n            OptionName.BranchName\n        ]\n\n    let private shouldShowResolvedValues (parseResult: ParseResult) =\n        resolvedValueOptionNames\n        |> List.exists (isOptionPresent parseResult)\n\n    let private tryBuildResolvedValuesText (parseResult: ParseResult) =\n        if\n            isNull parseResult\n            || not (configurationFileExists ())\n            || not (shouldShowResolvedValues parseResult)\n        then\n            None\n        else\n            let graceIds = Services.getNormalizedIdsAndNames parseResult\n            let sb = stringBuilderPool.Get()\n\n            try\n                let appendLine label value = sb.AppendLine($\"{label}: {value}\") |> ignore\n\n                let appendName label (value: string) = if not <| String.IsNullOrWhiteSpace(value) then appendLine label value\n\n                if graceIds.HasOwner then\n                    appendLine \"OwnerId\" graceIds.OwnerId\n                    appendName \"OwnerName\" graceIds.OwnerName\n\n                if graceIds.HasOrganization then\n                    appendLine \"OrganizationId\" graceIds.OrganizationId\n                    appendName \"OrganizationName\" graceIds.OrganizationName\n\n                if graceIds.HasRepository then\n                    appendLine \"RepositoryId\" graceIds.RepositoryId\n                    appendName \"RepositoryName\" graceIds.RepositoryName\n\n                if graceIds.HasBranch then\n                    appendLine \"BranchId\" graceIds.BranchId\n                    appendName \"BranchName\" graceIds.BranchName\n\n                if sb.Length > 0 then Some(sb.ToString()) else None\n            finally\n                stringBuilderPool.Return sb\n\n    /// Prints the ParseResult with markup.\n    let printParseResult (parseResult: ParseResult) =\n        if not <| isNull parseResult then\n            let sb = stringBuilderPool.Get()\n\n            try\n                // Gather all options from the root command and the invoked command.\n                let optionList =\n                    parseResult.RootCommandResult.Command.Options\n                    |> Seq.append parseResult.CommandResult.Command.Options\n                    |> Seq.sortBy (fun option -> option.Name)\n                    |> Seq.toIReadOnlyList\n\n                let tryGetValue (option: Option) =\n                    let result = parseResult.GetResult(option.Name)\n\n                    if isNull result then\n                        None\n                    else\n                        try\n                            let value = parseResult.GetValue(option.Name)\n                            if isNull value then None else Some value\n                        with\n                        | :? InvalidOperationException -> None\n\n                for option in optionList do\n                    match tryGetValue option with\n                    | Some value ->\n                        if option.ValueType.IsArray then\n                            sb.AppendLine($\"{option.Name}: {serialize value}\")\n                            |> ignore\n                        else\n                            sb.AppendLine($\"{option.Name}: {value}\") |> ignore\n                    | None -> ()\n\n                AnsiConsole.MarkupLine($\"[{Colors.Verbose}]{escapeBrackets (parseResult.ToString())}[/]\")\n                AnsiConsole.WriteLine()\n                AnsiConsole.MarkupLine($\"[{Colors.Verbose}]Parameter values:[/]\")\n                AnsiConsole.MarkupLine($\"[{Colors.Verbose}]{escapeBrackets (sb.ToString())}[/]\")\n                AnsiConsole.WriteLine()\n\n                match tryBuildResolvedValuesText parseResult with\n                | Some resolvedValues ->\n                    AnsiConsole.MarkupLine($\"[{Colors.Verbose}]Resolved values:[/]\")\n                    AnsiConsole.MarkupLine($\"[{Colors.Verbose}]{escapeBrackets resolvedValues}[/]\")\n                    AnsiConsole.WriteLine()\n                | None -> ()\n            finally\n                stringBuilderPool.Return sb\n\n    /// Prints AnsiConsole markup to the console.\n    let writeMarkup (markup: IRenderable) =\n        AnsiConsole.Write(markup)\n        AnsiConsole.WriteLine()\n\n    /// Prints output to the console, depending on the output format.\n    let renderOutput (parseResult: ParseResult) (result: GraceResult<'T>) =\n        let outputFormat =\n            discriminatedUnionFromString<OutputFormat>(\n                parseResult.GetValue(Options.output)\n            )\n                .Value\n\n        match result with\n        | Ok graceReturnValue ->\n            match outputFormat with\n            | Json -> AnsiConsole.WriteLine(Markup.Escape($\"{graceReturnValue}\"))\n            | Minimal -> () //AnsiConsole.MarkupLine($\"\"\"[{Colors.Highlighted}]{Markup.Escape($\"{graceReturnValue.ReturnValue}\")}[/]\"\"\")\n            | Silent -> ()\n            | Verbose ->\n                AnsiConsole.WriteLine()\n\n                AnsiConsole.MarkupLine($\"\"\"[{Colors.Verbose}]EventTime: {formatInstantExtended graceReturnValue.EventTime}[/]\"\"\")\n\n                AnsiConsole.MarkupLine($\"\"\"[{Colors.Verbose}]CorrelationId: \"{graceReturnValue.CorrelationId}\"[/]\"\"\")\n\n                AnsiConsole.MarkupLine($\"\"\"[{Colors.Verbose}]Properties: {Markup.Escape(serialize graceReturnValue.Properties)}[/]\"\"\")\n\n                AnsiConsole.WriteLine()\n            | Normal -> () // Return unit because in the Normal case, we expect to print output within each command.\n\n            0\n        | Error error ->\n            let json =\n                if error.Error.Contains(\"Stack trace\") then\n                    Uri.UnescapeDataString(error.Error)\n                else\n                    Uri.UnescapeDataString(serialize error)\n\n            let errorText =\n                if error.Error.Contains(\"Stack trace\") then\n                    try\n                        let exceptionResponse = deserialize<ExceptionResponse> error.Error\n                        Uri.UnescapeDataString($\"{exceptionResponse}\")\n                    with\n                    | ex -> Uri.UnescapeDataString(error.Error)\n                else\n                    Uri.UnescapeDataString(error.Error)\n\n            match outputFormat with\n            | Json -> AnsiConsole.WriteLine($\"{Markup.Escape(json)}\")\n            | Minimal -> AnsiConsole.MarkupLine($\"[{Colors.Error}]{Markup.Escape(errorText)}[/]\")\n            | Silent -> ()\n            | Verbose ->\n                AnsiConsole.MarkupLine($\"[{Colors.Error}]{Markup.Escape(errorText)}[/]\")\n                AnsiConsole.WriteLine()\n                AnsiConsole.MarkupLine($\"[{Colors.Verbose}]{Markup.Escape(json)}[/]\")\n                AnsiConsole.WriteLine()\n            | Normal -> AnsiConsole.MarkupLine($\"[{Colors.Error}]{Markup.Escape(errorText)}[/]\")\n\n            -1\n\n    let progressBarColumn = new ProgressBarColumn()\n    progressBarColumn.FinishedStyle <- new Style(foreground = Color.Green)\n\n    let percentageColumn = new PercentageColumn()\n    percentageColumn.Style <- new Style(foreground = Color.Yellow)\n    percentageColumn.CompletedStyle <- new Style(foreground = Color.Yellow)\n\n    let spinnerColumn = new SpinnerColumn(Spinner.Known.Dots)\n\n    let progressColumns: ProgressColumn [] =\n        [|\n            new TaskDescriptionColumn(Alignment = Justify.Right)\n            progressBarColumn\n            percentageColumn\n            spinnerColumn\n        |]\n\n    let progress = AnsiConsole.Progress(AutoRefresh = true, AutoClear = false, HideCompleted = false)\n"
  },
  {
    "path": "src/Grace.CLI/Command/Config.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen DiffPlex\nopen DiffPlex.DiffBuilder.Model\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Types.Branch\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Spectre.Console\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Linq\nopen System.IO\nopen System.Text.Json\nopen System.Threading.Tasks\n\nmodule Config =\n\n    type CommonParameters() =\n        inherit ParameterBase()\n        member val public Directory: string = \".\" with get, set\n        member val public Overwrite: bool = false with get, set\n\n    module private Options =\n        let directory =\n            new Option<string>(\n                OptionName.Directory,\n                Required = false,\n                Description = \"The root path of the repository to initialize Grace in [default: current directory]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> \".\")\n            )\n\n        let overwrite =\n            new Option<bool>(\n                OptionName.Overwrite,\n                Required = false,\n                Description = \"Allows Grace to overwrite an existing graceconfig.json file with default values\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n    let private CommonValidations parseResult =\n        let ``Directory must be a valid path`` (parseResult: ParseResult) =\n            let directory = parseResult.GetValue(Options.directory)\n\n            if Directory.Exists(directory) then\n                Ok parseResult\n            else\n                Error(GraceError.Create (getErrorMessage ConfigError.InvalidDirectoryPath) (getCorrelationId parseResult))\n\n        parseResult |> ``Directory must be a valid path``\n\n    let private renderLine (diffLine: DiffPiece) =\n        if not <| diffLine.Position.HasValue then\n            $\"        {diffLine.Text.EscapeMarkup()}\"\n        else\n            $\"{diffLine.Position, 6:D}: {diffLine.Text.EscapeMarkup()}\"\n\n    let private getMarkup (diffLine: DiffPiece) =\n        if diffLine.Type = ChangeType.Deleted then\n            Markup($\"[{Colors.Deleted}]-{renderLine diffLine}[/]\")\n        elif diffLine.Type = ChangeType.Inserted then\n            Markup($\"[{Colors.Added}]+{renderLine diffLine}[/]\")\n        elif diffLine.Type = ChangeType.Modified then\n            Markup($\"[{Colors.Changed}]~{renderLine diffLine}[/]\")\n        elif diffLine.Type = ChangeType.Imaginary then\n            Markup($\"[{Colors.Deemphasized}] {renderLine diffLine}[/]\")\n        elif diffLine.Type = ChangeType.Unchanged then\n            Markup($\"[{Colors.Important}] {renderLine diffLine}[/]\")\n        else\n            Markup($\"[{Colors.Important}] {diffLine.Text}[/]\")\n\n    type WriteParameters() =\n        inherit CommonParameters()\n\n    let writeHandler (parseResult: ParseResult) (parameters: WriteParameters) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n\n            let validateIncomingParameters = parseResult |> CommonValidations\n\n            match validateIncomingParameters with\n            | Ok _ ->\n                // Search for existing .grace directory and existing graceconfig.json\n                // If I find them, and parameters.Overwrite is true, then I can empty out the .grace directory and write a default graceconfig.json.\n                // If I don't find them, I should create the .grace directory and write a default graceconfig.json.\n                // We should use `GraceConfiguration() |> saveConfigFile parameters.Directory` to write the default config\n                let graceDirPath = Path.Combine(parameters.Directory, \".grace\")\n                let graceConfigPath = Path.Combine(graceDirPath, \"graceconfig.json\")\n\n                let overwriteExisting =\n                    parameters.Overwrite\n                    && Directory.Exists(graceDirPath)\n                    && File.Exists(graceConfigPath)\n\n                if overwriteExisting then\n                    // Clear out everything in the .grace directory.\n                    if parseResult |> hasOutput then\n                        printfn \"Deleting contents of existing .grace directory.\"\n\n                    Directory.Delete(graceDirPath, recursive = true)\n\n                if\n                    File.Exists(graceConfigPath)\n                    && not parameters.Overwrite\n                then\n                    if parseResult |> hasOutput then\n                        printfn\n                            $\"Found existing Grace configuration file at {graceConfigPath}. Specify {OptionName.Overwrite} if you'd like to overwrite it.{Environment.NewLine}\"\n                else\n                    let directoryInfo = Directory.CreateDirectory(graceDirPath)\n\n                    if parseResult |> hasOutput then\n                        printfn $\"Writing new Grace configuration file at {graceConfigPath}.{Environment.NewLine}\"\n\n                    GraceConfiguration()\n                    |> saveConfigFile graceConfigPath\n\n                return Ok(GraceReturnValue.Create () parameters.CorrelationId)\n            | Error error -> return (Error error)\n        }\n\n    type Write() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> CommonValidations\n                let directory = parseResult.GetValue(Options.directory)\n                let overwrite = parseResult.GetValue(Options.overwrite)\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    // Search for existing .grace directory and existing graceconfig.json\n                    // If I find them, and parameters.Overwrite is true, then I can empty out the .grace directory and write a default graceconfig.json.\n                    // If I don't find them, I should create the .grace directory and write a default graceconfig.json.\n                    // We should use `GraceConfiguration() |> saveConfigFile parameters.Directory` to write the default config\n                    let graceDirPath = Path.Combine(directory, \".grace\")\n                    let graceConfigPath = Path.Combine(graceDirPath, \"graceconfig.json\")\n\n                    let overwriteExisting =\n                        overwrite\n                        && Directory.Exists(graceDirPath)\n                        && File.Exists(graceConfigPath)\n\n                    if overwriteExisting then\n                        // Clear out everything in the .grace directory.\n                        if parseResult |> hasOutput then\n                            printfn \"Deleting contents of existing .grace directory.\"\n\n                        Directory.Delete(graceDirPath, recursive = true)\n\n                    if File.Exists(graceConfigPath) && not overwrite then\n                        if parseResult |> hasOutput then\n                            printfn\n                                $\"Found existing Grace configuration file at {graceConfigPath}. Specify {OptionName.Overwrite} if you'd like to overwrite it.{Environment.NewLine}\"\n                    else\n                        let directoryInfo = Directory.CreateDirectory(graceDirPath)\n\n                        if parseResult |> hasOutput then\n                            printfn $\"Writing new Grace configuration file at {graceConfigPath}.{Environment.NewLine}\"\n\n                        GraceConfiguration()\n                        |> saveConfigFile graceConfigPath\n\n                    return\n                        Ok(GraceReturnValue.Create () (getCorrelationId parseResult))\n                        |> renderOutput parseResult\n                | Error error -> return (Error error) |> renderOutput parseResult\n            //let! writeResult = writeHandler parseResult\n            //return writeResult |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.directory\n            |> addOption Options.overwrite\n\n        let configCommand = new Command(\"config\", Description = \"Initializes a repository with the default Grace configuration.\")\n\n        let writeCommand =\n            new Command(\"write\", Description = \"Initializes a repository with a default Grace configuration.\")\n            |> addCommonOptions\n\n        writeCommand.Action <- Write()\n        configCommand.Subcommands.Add(writeCommand)\n\n        configCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Connect.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Utilities\nopen Grace.Types.Owner\nopen Grace.Types.Branch\nopen Grace.Types.Organization\nopen Grace.Types.Reference\nopen Grace.Types.Repository\nopen Grace.Types.Types\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen System\nopen System.Collections.Generic\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.IO\nopen System.Threading.Tasks\nopen System.CommandLine\nopen Spectre.Console\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Models\nopen System.IO.Compression\nopen Grace.CLI\n\nmodule Connect =\n\n    type CommonParameters() =\n        inherit ParameterBase()\n        member val public RepositoryId: string = String.Empty with get, set\n        member val public RepositoryName: string = String.Empty with get, set\n        member val public OwnerId: string = String.Empty with get, set\n        member val public OwnerName: string = String.Empty with get, set\n        member val public OrganizationId: string = String.Empty with get, set\n        member val public OrganizationName: string = String.Empty with get, set\n        member val public RetrieveDefaultBranch: bool = true with get, set\n\n    module private Options =\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(OptionName.OwnerName, Required = false, Description = \"The repository's owner name.\", Arity = ArgumentArity.ExactlyOne)\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let correlationId =\n            new Option<String>(\n                OptionName.CorrelationId,\n                [| \"-c\" |],\n                Required = false,\n                Description = \"CorrelationId to track this command throughout Grace. [default: new Guid]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let serverAddress =\n            new Option<String>(\n                OptionName.ServerAddress,\n                [| \"-s\" |],\n                Required = false,\n                Description = \"Address of the Grace server to connect to.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branchId =\n            new Option<BranchId>(\n                OptionName.BranchId,\n                [| \"-i\" |],\n                Required = false,\n                Description = \"The branch ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let branchName =\n            new Option<String>(OptionName.BranchName, [| \"-b\" |], Required = false, Description = \"The name of the branch.\", Arity = ArgumentArity.ExactlyOne)\n\n        let referenceType =\n            (new Option<String>(OptionName.ReferenceType, Required = false, Description = \"The type of reference.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(listCases<ReferenceType> ())\n\n        let referenceId =\n            new Option<ReferenceId>(OptionName.ReferenceId, [||], Required = false, Description = \"The reference ID <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n        let directoryVersionId =\n            new Option<DirectoryVersionId>(\n                OptionName.DirectoryVersionId,\n                [| \"-t\" |],\n                Required = false,\n                Description = \"The directory version ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let force =\n            new Option<bool>(\n                OptionName.Force,\n                [| \"-f\"; \"--force\" |],\n                Required = false,\n                Description = \"Overwrite conflicting files when connecting.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let retrieveDefaultBranch =\n            new Option<bool>(\n                OptionName.RetrieveDefaultBranch,\n                [||],\n                Required = false,\n                Description = \"True to retrieve the default branch after connecting; false to connect but not download any files.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> true)\n            )\n\n    module private Arguments =\n        let repositoryShortcut =\n            new Argument<string>(\"repository\", Description = \"Repository shortcut in the form owner/organization/repository.\", Arity = ArgumentArity.ZeroOrOne)\n\n    type DirectoryVersionSelection =\n        | UseDirectoryVersionId of DirectoryVersionId\n        | UseReferenceId of ReferenceId\n        | UseReferenceType of ReferenceType\n        | UseDefault\n\n    let private tryGetExplicitValue<'T> (parseResult: ParseResult) (option: Option<'T>) =\n        let result = parseResult.GetResult(option)\n\n        if isNull result || result.Implicit then\n            None\n        else\n            Some(parseResult.GetValue(option))\n\n    let private tryGetExplicitNonEmptyString (parseResult: ParseResult) (option: Option<string>) =\n        match tryGetExplicitValue parseResult option with\n        | Some value when not <| String.IsNullOrWhiteSpace(value) -> Some value\n        | _ -> None\n\n    type private RepositoryShortcut = { OwnerName: OwnerName; OrganizationName: OrganizationName; RepositoryName: RepositoryName }\n\n    let private validateGraceName (name: string) (error: IErrorDiscriminatedUnion) (parseResult: ParseResult) =\n        if Constants.GraceNameRegex.IsMatch(name) then\n            Ok name\n        else\n            Error(GraceError.Create (getErrorMessage error) (getCorrelationId parseResult))\n\n    let private tryGetRepositoryShortcut (parseResult: ParseResult) =\n        let result = parseResult.GetResult(Arguments.repositoryShortcut)\n\n        if isNull result || result.Implicit then\n            Ok None\n        else\n            let value = parseResult.GetValue(Arguments.repositoryShortcut)\n\n            if String.IsNullOrWhiteSpace(value) then\n                Error(GraceError.Create \"Repository shortcut must be in the form owner/organization/repository.\" (getCorrelationId parseResult))\n            else\n                let parts =\n                    value\n                        .Trim()\n                        .Split('/', StringSplitOptions.RemoveEmptyEntries)\n\n                if parts.Length <> 3 then\n                    Error(GraceError.Create \"Repository shortcut must be in the form owner/organization/repository.\" (getCorrelationId parseResult))\n                else\n                    let ownerName = parts[ 0 ].Trim()\n                    let organizationName = parts[ 1 ].Trim()\n                    let repositoryName = parts[ 2 ].Trim()\n\n                    match validateGraceName ownerName OwnerError.InvalidOwnerName parseResult with\n                    | Error error -> Error error\n                    | Ok ownerName ->\n                        match validateGraceName organizationName OrganizationError.InvalidOrganizationName parseResult with\n                        | Error error -> Error error\n                        | Ok organizationName ->\n                            match validateGraceName repositoryName RepositoryError.InvalidRepositoryName parseResult with\n                            | Error error -> Error error\n                            | Ok repositoryName -> Ok(Some { OwnerName = ownerName; OrganizationName = organizationName; RepositoryName = repositoryName })\n\n    let private hasExplicitOwner (parseResult: ParseResult) =\n        tryGetExplicitValue parseResult Options.ownerId\n        |> Option.exists (fun ownerId -> ownerId <> Guid.Empty)\n        || (tryGetExplicitNonEmptyString parseResult Options.ownerName\n            |> Option.isSome)\n\n    let private hasExplicitOrganization (parseResult: ParseResult) =\n        tryGetExplicitValue parseResult Options.organizationId\n        |> Option.exists (fun organizationId -> organizationId <> Guid.Empty)\n        || (tryGetExplicitNonEmptyString parseResult Options.organizationName\n            |> Option.isSome)\n\n    let private hasExplicitRepository (parseResult: ParseResult) =\n        tryGetExplicitValue parseResult Options.repositoryId\n        |> Option.exists (fun repositoryId -> repositoryId <> Guid.Empty)\n        || (tryGetExplicitNonEmptyString parseResult Options.repositoryName\n            |> Option.isSome)\n\n    let internal applyRepositoryShortcut (parseResult: ParseResult) (graceIds: GraceIds) =\n        match tryGetRepositoryShortcut parseResult with\n        | Error error -> Error error\n        | Ok None -> Ok graceIds\n        | Ok (Some shortcut) ->\n            if hasExplicitOwner parseResult\n               || hasExplicitOrganization parseResult\n               || hasExplicitRepository parseResult then\n                Error(\n                    GraceError.Create\n                        \"Provide either the repository shortcut or the owner/organization/repository options, not both.\"\n                        (getCorrelationId parseResult)\n                )\n            else\n                Ok\n                    { graceIds with\n                        OwnerId = Guid.Empty\n                        OwnerIdString = String.Empty\n                        OwnerName = shortcut.OwnerName\n                        OrganizationId = Guid.Empty\n                        OrganizationIdString = String.Empty\n                        OrganizationName = shortcut.OrganizationName\n                        RepositoryId = Guid.Empty\n                        RepositoryIdString = String.Empty\n                        RepositoryName = shortcut.RepositoryName\n                        HasOwner = true\n                        HasOrganization = true\n                        HasRepository = true\n                    }\n\n    let internal getDirectoryVersionSelection (parseResult: ParseResult) =\n        match tryGetExplicitValue parseResult Options.directoryVersionId with\n        | Some directoryVersionId when directoryVersionId <> Guid.Empty -> UseDirectoryVersionId directoryVersionId\n        | _ ->\n            match tryGetExplicitValue parseResult Options.referenceId with\n            | Some referenceId when referenceId <> Guid.Empty -> UseReferenceId referenceId\n            | _ ->\n                match tryGetExplicitNonEmptyString parseResult Options.referenceType with\n                | Some referenceTypeRaw ->\n                    let referenceType =\n                        discriminatedUnionFromString<ReferenceType>(\n                            referenceTypeRaw\n                        )\n                            .Value\n\n                    UseReferenceType referenceType\n                | None -> UseDefault\n\n    let internal tryGetDirectoryIdFromBranch (referenceType: ReferenceType) (branchDto: BranchDto) =\n        match referenceType with\n        | ReferenceType.Promotion when\n            branchDto.LatestPromotion.DirectoryId\n            <> Guid.Empty\n            ->\n            Some branchDto.LatestPromotion.DirectoryId\n        | ReferenceType.Commit when branchDto.LatestCommit.DirectoryId <> Guid.Empty -> Some branchDto.LatestCommit.DirectoryId\n        | ReferenceType.Checkpoint when\n            branchDto.LatestCheckpoint.DirectoryId\n            <> Guid.Empty\n            ->\n            Some branchDto.LatestCheckpoint.DirectoryId\n        | ReferenceType.Save when branchDto.LatestSave.DirectoryId <> Guid.Empty -> Some branchDto.LatestSave.DirectoryId\n        | _ -> None\n\n    let internal resolveDefaultDirectoryVersionId (branchDto: BranchDto) =\n        if branchDto.LatestPromotion.DirectoryId\n           <> Guid.Empty then\n            Some branchDto.LatestPromotion.DirectoryId\n        elif branchDto.BasedOn.DirectoryId <> Guid.Empty then\n            Some branchDto.BasedOn.DirectoryId\n        else\n            None\n\n    let private selectLatestReference (references: ReferenceDto seq) =\n        references\n        |> Seq.sortByDescending (fun reference ->\n            reference.UpdatedAt\n            |> Option.defaultValue reference.CreatedAt)\n        |> Seq.tryHead\n\n    let private resolveDirectoryVersionIdFromReferenceType\n        (graceIds: GraceIds)\n        (ownerDto: OwnerDto)\n        (organizationDto: OrganizationDto)\n        (repositoryDto: RepositoryDto)\n        (branchDto: BranchDto)\n        (referenceType: ReferenceType)\n        =\n        task {\n            match tryGetDirectoryIdFromBranch referenceType branchDto with\n            | Some directoryId -> return Ok directoryId\n            | None ->\n                let getReferencesParameters =\n                    Parameters.Branch.GetReferencesParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OwnerName = ownerDto.OwnerName,\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        OrganizationName = organizationDto.OrganizationName,\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        RepositoryName = repositoryDto.RepositoryName,\n                        BranchId = $\"{branchDto.BranchId}\",\n                        BranchName = branchDto.BranchName,\n                        MaxCount = 50,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                let referencesTask =\n                    match referenceType with\n                    | ReferenceType.Tag -> Branch.GetTags(getReferencesParameters)\n                    | ReferenceType.External -> Branch.GetExternals(getReferencesParameters)\n                    | ReferenceType.Rebase -> Branch.GetRebases(getReferencesParameters)\n                    | _ -> Task.FromResult(Ok(GraceReturnValue.Create [||] graceIds.CorrelationId))\n\n                let! referencesResult = referencesTask\n\n                match referencesResult with\n                | Ok returnValue ->\n                    match selectLatestReference returnValue.ReturnValue with\n                    | Some reference -> return Ok reference.DirectoryId\n                    | None ->\n                        return Error(GraceError.Create $\"No {referenceType} references were found for branch {branchDto.BranchName}.\" graceIds.CorrelationId)\n                | Error error -> return Error error\n        }\n\n    let private resolveTargetDirectoryVersionId\n        (parseResult: ParseResult)\n        (graceIds: GraceIds)\n        (ownerDto: OwnerDto)\n        (organizationDto: OrganizationDto)\n        (repositoryDto: RepositoryDto)\n        (branchDto: BranchDto)\n        =\n        task {\n            match getDirectoryVersionSelection parseResult with\n            | UseDirectoryVersionId directoryVersionId -> return Ok directoryVersionId\n            | UseReferenceId referenceId ->\n                let getReferenceParameters =\n                    Parameters.Branch.GetReferenceParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OwnerName = ownerDto.OwnerName,\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        OrganizationName = organizationDto.OrganizationName,\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        RepositoryName = repositoryDto.RepositoryName,\n                        BranchId = $\"{branchDto.BranchId}\",\n                        BranchName = branchDto.BranchName,\n                        ReferenceId = $\"{referenceId}\",\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                let! referenceResult = Branch.GetReference(getReferenceParameters)\n\n                return\n                    match referenceResult with\n                    | Ok returnValue -> Ok returnValue.ReturnValue.DirectoryId\n                    | Error error -> Error error\n            | UseReferenceType referenceType ->\n                return! resolveDirectoryVersionIdFromReferenceType graceIds ownerDto organizationDto repositoryDto branchDto referenceType\n            | UseDefault ->\n                match resolveDefaultDirectoryVersionId branchDto with\n                | Some directoryVersionId -> return Ok directoryVersionId\n                | None -> return Error(GraceError.Create \"No downloadable version found for this branch.\" graceIds.CorrelationId)\n        }\n\n    let private collectFileConflicts (fileVersions: FileVersion array) (force: bool) =\n        let conflicts = ResizeArray<string>()\n        let filesToSkip = HashSet<RelativePath>()\n\n        let rec loop index =\n            task {\n                if index >= fileVersions.Length then\n                    return conflicts, filesToSkip\n                else\n                    let fileVersion = fileVersions[index]\n                    let filePath = Path.Combine(Current().RootDirectory, fileVersion.RelativePath)\n\n                    if File.Exists(filePath) then\n                        try\n                            use stream = File.OpenRead(filePath)\n\n                            let! localHash = Grace.Shared.Services.computeSha256ForFile stream fileVersion.RelativePath\n\n                            if localHash = fileVersion.Sha256Hash then\n                                filesToSkip.Add(fileVersion.RelativePath)\n                                |> ignore\n                            elif not force then\n                                conflicts.Add(fileVersion.RelativePath)\n                        with\n                        | _ -> if not force then conflicts.Add(fileVersion.RelativePath)\n\n                    return! loop (index + 1)\n            }\n\n        loop 0\n\n    let private ensureConfigurationFileExists () =\n        if not <| configurationFileExists () then\n            let graceDirPath = Path.Combine(Environment.CurrentDirectory, Constants.GraceConfigDirectory)\n            let graceConfigPath = Path.Combine(graceDirPath, Constants.GraceConfigFileName)\n            Directory.CreateDirectory(graceDirPath) |> ignore\n\n            if not <| File.Exists(graceConfigPath) then\n                GraceConfiguration()\n                |> saveConfigFile graceConfigPath\n\n    let private reloadConfiguration () =\n        resetConfiguration ()\n        Current() |> ignore\n\n    let private applyServerAddressOverride (parseResult: ParseResult) =\n        match tryGetExplicitNonEmptyString parseResult Options.serverAddress with\n        | Some serverAddress ->\n            let newConfig = Current()\n            newConfig.ServerUri <- serverAddress\n            updateConfiguration newConfig\n            reloadConfiguration ()\n        | None -> ()\n\n    let private validateRequiredIds (parseResult: ParseResult) (graceIds: GraceIds) =\n        let correlationId = getCorrelationId parseResult\n\n        let ownerValid =\n            graceIds.OwnerId <> Guid.Empty\n            || not\n               <| String.IsNullOrWhiteSpace(graceIds.OwnerName)\n\n        let organizationValid =\n            graceIds.OrganizationId <> Guid.Empty\n            || not\n               <| String.IsNullOrWhiteSpace(graceIds.OrganizationName)\n\n        let repositoryValid =\n            graceIds.RepositoryId <> Guid.Empty\n            || not\n               <| String.IsNullOrWhiteSpace(graceIds.RepositoryName)\n\n        if not ownerValid then\n            Error(GraceError.Create (getErrorMessage OwnerError.EitherOwnerIdOrOwnerNameRequired) correlationId)\n        elif not organizationValid then\n            Error(GraceError.Create (getErrorMessage OrganizationError.EitherOrganizationIdOrOrganizationNameRequired) correlationId)\n        elif not repositoryValid then\n            Error(GraceError.Create (getErrorMessage RepositoryError.EitherRepositoryIdOrRepositoryNameRequired) correlationId)\n        else\n            Ok()\n\n    let private getOwnerOrganizationRepository (graceIds: GraceIds) =\n        task {\n            let ownerParameters =\n                Parameters.Owner.GetOwnerParameters(OwnerId = graceIds.OwnerIdString, OwnerName = graceIds.OwnerName, CorrelationId = graceIds.CorrelationId)\n\n            let! ownerResult = Grace.SDK.Owner.Get(ownerParameters)\n\n            let organizationParameters =\n                Parameters.Organization.GetOrganizationParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            let! organizationResult = Organization.Get(organizationParameters)\n\n            let repositoryParameters =\n                Parameters.Repository.GetRepositoryParameters(\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            let! repositoryResult = Repository.Get(repositoryParameters)\n\n            match (ownerResult, organizationResult, repositoryResult) with\n            | (Ok owner, Ok organization, Ok repository) -> return Ok(owner.ReturnValue, organization.ReturnValue, repository.ReturnValue)\n            | (Error error, _, _) -> return Error error\n            | (_, Error error, _) -> return Error error\n            | (_, _, Error error) -> return Error error\n        }\n\n    let private getBranchForConnect\n        (parseResult: ParseResult)\n        (graceIds: GraceIds)\n        (ownerDto: OwnerDto)\n        (organizationDto: OrganizationDto)\n        (repositoryDto: RepositoryDto)\n        =\n        task {\n            let branchId =\n                tryGetExplicitValue parseResult Options.branchId\n                |> Option.filter (fun value -> value <> Guid.Empty)\n\n            let branchName = tryGetExplicitNonEmptyString parseResult Options.branchName\n\n            let branchParameters =\n                match branchId, branchName with\n                | Some id, _ ->\n                    Parameters.Branch.GetBranchParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        BranchId = $\"{id}\",\n                        CorrelationId = graceIds.CorrelationId\n                    )\n                | None, Some name ->\n                    Parameters.Branch.GetBranchParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        BranchName = name,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n                | None, None ->\n                    Parameters.Branch.GetBranchParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        BranchName = $\"{repositoryDto.DefaultBranchName}\",\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n            let! branchResult = Branch.Get(branchParameters)\n\n            return\n                match branchResult with\n                | Ok graceReturnValue -> Ok graceReturnValue.ReturnValue\n                | Error error -> Error error\n        }\n\n    let private buildFileVersionsByRelativePath (fileVersions: FileVersion array) =\n        let lookup = Dictionary<RelativePath, FileVersion>(fileVersions.Length, StringComparer.OrdinalIgnoreCase)\n\n        fileVersions\n        |> Seq.iter (fun fileVersion -> lookup[normalizeFilePath fileVersion.RelativePath] <- fileVersion)\n\n        lookup\n\n    let private extractZipEntries\n        (parseResult: ParseResult)\n        (fileVersionsByRelativePath: Dictionary<RelativePath, FileVersion>)\n        (filesToSkip: HashSet<RelativePath>)\n        (zipFile: Stream)\n        =\n        use zipFile = zipFile\n        use zipArchive = new ZipArchive(zipFile, ZipArchiveMode.Read)\n\n        AnsiConsole.MarkupLine $\"[{Colors.Important}]Streaming contents from .zip file.[/]\"\n        AnsiConsole.MarkupLine $\"[{Colors.Important}]Starting to write files to disk.[/]\"\n\n        let additionalEntries = ResizeArray<string>()\n\n        zipArchive.Entries\n        |> Seq.iter (fun entry ->\n            if not <| String.IsNullOrEmpty(entry.Name) then\n                let entryRelativePath = normalizeFilePath entry.FullName\n\n                match fileVersionsByRelativePath.TryGetValue(entryRelativePath) with\n                | true, fileVersion ->\n                    let objectFileName =\n                        if String.IsNullOrWhiteSpace(entry.Comment) then\n                            fileVersion.GetObjectFileName\n                        else\n                            entry.Comment\n\n                    let fileInfo = FileInfo(Path.Combine(Current().RootDirectory, fileVersion.RelativePath))\n\n                    let objectFileInfo = FileInfo(Path.Combine(Current().ObjectDirectory, fileVersion.RelativePath, objectFileName))\n\n                    Directory.CreateDirectory(fileInfo.DirectoryName)\n                    |> ignore\n\n                    Directory.CreateDirectory(objectFileInfo.DirectoryName)\n                    |> ignore\n\n                    let writeWorkingFile =\n                        not\n                        <| filesToSkip.Contains(fileVersion.RelativePath)\n\n                    let writeObjectFile = not objectFileInfo.Exists\n\n                    if fileVersion.IsBinary then\n                        if writeWorkingFile then entry.ExtractToFile(fileInfo.FullName, true)\n                        if writeObjectFile then entry.ExtractToFile(objectFileInfo.FullName, true)\n                    else\n                        let uncompressAndWriteToFile (zipEntry: ZipArchiveEntry) (fileInfo: FileInfo) =\n                            use entryStream = zipEntry.Open()\n                            use fileStream = fileInfo.Create()\n                            use gzipStream = new GZipStream(entryStream, CompressionMode.Decompress)\n                            gzipStream.CopyTo(fileStream)\n\n                        if writeWorkingFile then uncompressAndWriteToFile entry fileInfo\n                        if writeObjectFile then uncompressAndWriteToFile entry objectFileInfo\n\n                    if parseResult |> verbose then\n                        AnsiConsole.MarkupLine $\"[{Colors.Important}]Wrote {fileVersion.RelativePath}.[/]\"\n                | false, _ -> additionalEntries.Add(entry.FullName))\n\n        if additionalEntries.Count > 0\n           && (parseResult |> verbose) then\n            AnsiConsole.MarkupLine $\"[{Colors.Deemphasized}]Zip contained {additionalEntries.Count} additional entry(ies). Ignored.[/]\"\n\n        AnsiConsole.MarkupLine $\"[{Colors.Important}]Finished writing files to disk.[/]\"\n\n    let private retrieveDefaultBranchAndWrite\n        (parseResult: ParseResult)\n        (graceIds: GraceIds)\n        (ownerDto: OwnerDto)\n        (organizationDto: OrganizationDto)\n        (repositoryDto: RepositoryDto)\n        (branchDto: BranchDto)\n        =\n        task {\n            let! directoryVersionResult = resolveTargetDirectoryVersionId parseResult graceIds ownerDto organizationDto repositoryDto branchDto\n\n            match directoryVersionResult with\n            | Error error -> return (Error error |> renderOutput parseResult)\n            | Ok directoryVersionId ->\n                let getDirectoryContentsParameters =\n                    Parameters.DirectoryVersion.GetParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        DirectoryVersionId = $\"{directoryVersionId}\",\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                AnsiConsole.MarkupLine $\"[{Colors.Important}]Retrieving all DirectoryVersions.[/]\"\n\n                let! directoryVersionsResult = DirectoryVersion.GetDirectoryVersionsRecursive(getDirectoryContentsParameters)\n\n                let getZipFileParameters =\n                    Parameters.DirectoryVersion.GetZipFileParameters(\n                        OwnerId = $\"{ownerDto.OwnerId}\",\n                        OrganizationId = $\"{organizationDto.OrganizationId}\",\n                        RepositoryId = $\"{repositoryDto.RepositoryId}\",\n                        DirectoryVersionId = $\"{directoryVersionId}\",\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                AnsiConsole.MarkupLine $\"[{Colors.Important}]Retrieving zip file download uri.[/]\"\n                let! getZipFileResult = DirectoryVersion.GetZipFile(getZipFileParameters)\n                AnsiConsole.MarkupLine $\"[{Colors.Important}]Finished getting zip file download uri.[/]\"\n\n                match (directoryVersionsResult, getZipFileResult) with\n                | (Ok directoryVerionsReturnValue, Ok getZipFileReturnValue) ->\n                    AnsiConsole.MarkupLine $\"[{Colors.Important}]Retrieved all DirectoryVersions.[/]\"\n\n                    let directoryVersionDtos = directoryVerionsReturnValue.ReturnValue\n\n                    let fileVersions =\n                        directoryVersionDtos\n                        |> Seq.map (fun directoryVersionDto -> directoryVersionDto.DirectoryVersion)\n                        |> Seq.collect (fun dv -> dv.Files)\n                        |> Seq.toArray\n\n                    let force = parseResult.GetValue(Options.force)\n\n                    let! conflicts, filesToSkip = collectFileConflicts fileVersions force\n\n                    if conflicts.Count > 0 then\n                        AnsiConsole.MarkupLine $\"[{Colors.Error}]Found {conflicts.Count} conflicting file(s). Use --force to overwrite.[/]\"\n\n                        if parseResult |> verbose then\n                            conflicts\n                            |> Seq.sort\n                            |> Seq.iter (fun conflict -> AnsiConsole.MarkupLine $\"[{Colors.Error}]{conflict}[/]\")\n\n                        return\n                            (Error(GraceError.Create \"Conflicting files exist in the working directory.\" graceIds.CorrelationId)\n                             |> renderOutput parseResult)\n                    else\n                        let fileVersionsByRelativePath = buildFileVersionsByRelativePath fileVersions\n\n                        let uriWithSharedAccessSignature = getZipFileReturnValue.ReturnValue\n\n                        // Download the .zip file to temp directory.\n                        let blobClient = BlobClient(uriWithSharedAccessSignature)\n\n                        let! zipFile = blobClient.OpenReadAsync(bufferSize = 64 * 1024)\n                        extractZipEntries parseResult fileVersionsByRelativePath filesToSkip zipFile\n\n                        AnsiConsole.MarkupLine $\"[{Colors.Important}]Creating Grace Index file.[/]\"\n                        let! previousGraceStatus = readGraceStatusFile ()\n                        let! graceStatus = createNewGraceStatusFile previousGraceStatus parseResult\n                        do! writeGraceStatusFile graceStatus\n\n                        AnsiConsole.MarkupLine $\"[{Colors.Important}]Creating Grace Object Cache Index file.[/]\"\n                        do! upsertObjectCache graceStatus.Index.Values\n                        return 0\n                | (Error error, _) -> return (Error error |> renderOutput parseResult)\n                | (_, Error error) -> return (Error error |> renderOutput parseResult)\n        }\n\n    let private connectImpl (parseResult: ParseResult) : Task<int> =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            ensureConfigurationFileExists ()\n            reloadConfiguration ()\n            applyServerAddressOverride parseResult\n            let validateIncomingParameters = Validations.CommonValidations parseResult\n\n            match validateIncomingParameters with\n            | Error error -> return (Error error |> renderOutput parseResult)\n            | Ok _ ->\n                let graceIds = getNormalizedIdsAndNames parseResult\n\n                match applyRepositoryShortcut parseResult graceIds with\n                | Error error -> return (Error error |> renderOutput parseResult)\n                | Ok graceIds ->\n                    match validateRequiredIds parseResult graceIds with\n                    | Error error -> return (Error error |> renderOutput parseResult)\n                    | Ok () ->\n                        do! Auth.ensureAccessToken parseResult\n\n                        let! ownerOrgRepoResult = getOwnerOrganizationRepository graceIds\n\n                        match ownerOrgRepoResult with\n                        | Ok (ownerDto, organizationDto, repositoryDto) ->\n                            AnsiConsole.MarkupLine $\"[{Colors.Important}]Found owner, organization, and repository.[/]\"\n\n                            let! branchResult = getBranchForConnect parseResult graceIds ownerDto organizationDto repositoryDto\n\n                            match branchResult with\n                            | Ok branchDto ->\n                                AnsiConsole.MarkupLine $\"[{Colors.Important}]Retrieved branch {branchDto.BranchName}.[/]\"\n                                // Write the new configuration to the config file.\n                                let newConfig = Current()\n                                newConfig.OwnerId <- ownerDto.OwnerId\n                                newConfig.OwnerName <- ownerDto.OwnerName\n                                newConfig.OrganizationId <- organizationDto.OrganizationId\n                                newConfig.OrganizationName <- organizationDto.OrganizationName\n                                newConfig.RepositoryId <- repositoryDto.RepositoryId\n                                newConfig.RepositoryName <- repositoryDto.RepositoryName\n                                newConfig.BranchId <- branchDto.BranchId\n                                newConfig.BranchName <- branchDto.BranchName\n                                newConfig.DefaultBranchName <- repositoryDto.DefaultBranchName\n                                newConfig.ObjectStorageProvider <- repositoryDto.ObjectStorageProvider\n                                updateConfiguration newConfig\n                                reloadConfiguration ()\n                                AnsiConsole.MarkupLine $\"[{Colors.Important}]Wrote new Grace configuration file.[/]\"\n\n                                let retrieveDefaultBranch = parseResult.GetValue(Options.retrieveDefaultBranch)\n\n                                if retrieveDefaultBranch then\n                                    return! retrieveDefaultBranchAndWrite parseResult graceIds ownerDto organizationDto repositoryDto branchDto\n                                else\n                                    return 0\n                            | Error error -> return (Error error |> renderOutput parseResult)\n                        | Error error -> return (Error error |> renderOutput parseResult)\n        }\n\n    type Connect() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                try\n                    return! connectImpl parseResult\n                with\n                | :? OperationCanceledException -> return -1\n                | ex ->\n                    let error = GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult)\n                    return (Error error |> renderOutput parseResult)\n            }\n\n    let Build =\n        // Create main command and aliases, if any.\n        let connectCommand = new Command(\"connect\", Description = \"Connect to a Grace repository.\")\n\n        connectCommand.Arguments.Add(Arguments.repositoryShortcut)\n        connectCommand.Options.Add(Options.repositoryId)\n        connectCommand.Options.Add(Options.repositoryName)\n        connectCommand.Options.Add(Options.ownerId)\n        connectCommand.Options.Add(Options.ownerName)\n        connectCommand.Options.Add(Options.organizationId)\n        connectCommand.Options.Add(Options.organizationName)\n        connectCommand.Options.Add(Options.branchId)\n        connectCommand.Options.Add(Options.branchName)\n        connectCommand.Options.Add(Options.referenceType)\n        connectCommand.Options.Add(Options.referenceId)\n        connectCommand.Options.Add(Options.directoryVersionId)\n        connectCommand.Options.Add(Options.correlationId)\n        connectCommand.Options.Add(Options.serverAddress)\n        connectCommand.Options.Add(Options.retrieveDefaultBranch)\n        connectCommand.Options.Add(Options.force)\n\n        connectCommand.Action <- Connect()\n        connectCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Diff.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen DiffPlex\nopen DiffPlex.DiffBuilder.Model\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.Branch\nopen Grace.Shared.Parameters.Diff\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Utilities\nopen Grace.Types.Branch\nopen Grace.Types.Diff\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen Grace.Shared.Validation.Errors\nopen Spectre.Console\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Linq\nopen System.IO\nopen System.Text.Json\nopen System.Threading\nopen System.Threading.Tasks\nopen Spectre.Console\nopen Spectre.Console\nopen Spectre.Console.Rendering\nopen Grace.Shared.Parameters.Storage\n\nmodule Diff =\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's Id <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branchId =\n            new Option<BranchId>(\n                OptionName.BranchId,\n                [| \"-i\" |],\n                Required = false,\n                Description = \"The branch's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let branchName =\n            new Option<string>(\n                OptionName.BranchName,\n                [| \"-b\" |],\n                Required = false,\n                Description = \"The name of the branch. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let directoryVersionId1 =\n            new Option<DirectoryVersionId>(\n                OptionName.DirectoryVersionId1,\n                [| OptionName.D1 |],\n                Required = true,\n                Description = \"The first DirectoryId to compare in the diff.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let directoryVersionId2 =\n            new Option<DirectoryVersionId>(\n                OptionName.DirectoryVersionId2,\n                [| OptionName.D2 |],\n                Required = false,\n                Description = \"The second DirectoryId to compare in the diff.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let sha256Hash1 =\n            new Option<Sha256Hash>(\n                OptionName.Sha256Hash1,\n                [| OptionName.S1 |],\n                Required = true,\n                Description = \"The first partial or full SHA-256 hash to compare in the diff.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let sha256Hash2 =\n            new Option<Sha256Hash>(\n                OptionName.Sha256Hash2,\n                [| OptionName.S2 |],\n                Required = false,\n                Description = \"The second partial or full SHA-256 hash to compare in the diff.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let tag =\n            new Option<string>(OptionName.Tag, Required = true, Description = \"The tag to compare the current version to.\", Arity = ArgumentArity.ExactlyOne)\n\n    let private sha256Validations parseResult =\n        let graceIds = getNormalizedIdsAndNames parseResult\n\n        let ``Sha256Hash1 must be a valid SHA-256 hash value`` (parseResult: ParseResult) =\n            if parseResult.GetResult(Options.sha256Hash1) <> null\n               && not\n                  <| Constants.Sha256Regex.IsMatch(parseResult.GetValue(Options.sha256Hash1)) then\n                let properties = Dictionary<string, obj>()\n                properties.Add(\"repositoryId\", graceIds.RepositoryId)\n                properties.Add(\"sha256Hash1\", parseResult.GetValue(Options.sha256Hash1))\n                properties.Add(\"sha256Hash2\", parseResult.GetValue(Options.sha256Hash2))\n\n                Error(GraceError.CreateWithMetadata null (getErrorMessage DiffError.InvalidSha256Hash) (graceIds.CorrelationId) properties)\n            else\n                Ok parseResult\n\n        let ``Sha256Hash2 must be a valid SHA-256 hash value`` (parseResult: ParseResult) =\n            if parseResult.GetResult(Options.sha256Hash2) <> null\n               && not\n                  <| Constants.Sha256Regex.IsMatch(parseResult.GetValue(Options.sha256Hash2)) then\n                let properties = Dictionary<string, obj>()\n                properties.Add(\"repositoryId\", graceIds.RepositoryId)\n                properties.Add(\"sha256Hash1\", parseResult.GetValue(Options.sha256Hash1))\n                properties.Add(\"sha256Hash2\", parseResult.GetValue(Options.sha256Hash2))\n\n                Error(GraceError.Create (getErrorMessage DiffError.InvalidSha256Hash) (graceIds.CorrelationId))\n            else\n                Ok parseResult\n\n        parseResult\n        |> ``Sha256Hash1 must be a valid SHA-256 hash value``\n        >>= ``Sha256Hash2 must be a valid SHA-256 hash value``\n\n    let private renderLine (diffLine: DiffPiece) =\n        if not <| diffLine.Position.HasValue then\n            $\"        {diffLine.Text.EscapeMarkup()}\"\n        else\n            $\"{diffLine.Position, 6:D}: {diffLine.Text.EscapeMarkup()}\"\n\n    let private getMarkup (diffLine: DiffPiece) =\n        match diffLine.Type with\n        | ChangeType.Deleted -> Markup($\"[{Colors.Deleted}]-{renderLine diffLine}[/]\")\n        | ChangeType.Inserted -> Markup($\"[{Colors.Added}]+{renderLine diffLine}[/]\")\n        | ChangeType.Modified -> Markup($\"[{Colors.Changed}]~{renderLine diffLine}[/]\")\n        | ChangeType.Imaginary -> Markup($\"[{Colors.Deemphasized}] {renderLine diffLine}[/]\")\n        | ChangeType.Unchanged -> Markup($\"[{Colors.Important}] {renderLine diffLine}[/]\")\n        | _ -> Markup($\"[{Colors.Important}] {diffLine.Text}[/]\")\n\n    let markupList = List<IRenderable>()\n    let addToOutput (markup: IRenderable) = markupList.Add markup\n\n    let renderInlineDiff (inlineDiff: List<DiffPiece []>) =\n        for i = 0 to inlineDiff.Count - 1 do\n            for diffLine in inlineDiff[i] do\n                addToOutput (getMarkup diffLine)\n\n            if not <| (i = inlineDiff.Count - 1) then\n                addToOutput (Markup($\"[{Colors.Deemphasized}]-------[/]\"))\n            else\n                addToOutput (Markup(String.Empty))\n\n    let printDiffResults (diffDto: DiffDto) =\n        if diffDto.HasDifferences then\n            addToOutput (Markup($\"[{Colors.Important}]Differences found.[/]\"))\n\n            for diff in diffDto.Differences do\n                match diff.FileSystemEntryType with\n                | FileSystemEntryType.File ->\n                    addToOutput (\n                        Markup($\"[{Colors.Important}]{getDiscriminatedUnionCaseName diff.DifferenceType}[/] [{Colors.Highlighted}]{diff.RelativePath}[/]\")\n                    )\n                | FileSystemEntryType.Directory ->\n                    if diff.DifferenceType <> DifferenceType.Change then\n                        addToOutput (\n                            Markup($\"[{Colors.Important}]{getDiscriminatedUnionCaseName diff.DifferenceType}[/] [{Colors.Highlighted}]{diff.RelativePath}[/]\")\n                        )\n\n            for fileDiff in diffDto.FileDiffs.OrderBy(fun fileDiff -> fileDiff.RelativePath) do\n                //addToOutput ((new Rule($\"[{Colors.Important}]{fileDiff.RelativePath}[/]\")).LeftAligned())\n                if fileDiff.CreatedAt1 > fileDiff.CreatedAt2 then\n                    addToOutput (\n                        (new Rule(\n                            $\"[{Colors.Important}]{fileDiff.RelativePath} | {getShortSha256Hash fileDiff.FileSha1} - {fileDiff.CreatedAt1 |> ago} | {getShortSha256Hash fileDiff.FileSha2} - {fileDiff.CreatedAt2 |> ago}[/]\"\n                        ))\n                            .LeftJustified()\n                    )\n                else\n                    addToOutput (\n                        (new Rule(\n                            $\"[{Colors.Important}]{fileDiff.RelativePath} | {getShortSha256Hash fileDiff.FileSha2} - {fileDiff.CreatedAt2 |> ago} | {getShortSha256Hash fileDiff.FileSha1} - {fileDiff.CreatedAt1 |> ago}[/]\"\n                        ))\n                            .LeftJustified()\n                    )\n\n                if fileDiff.IsBinary then\n                    addToOutput (Markup($\"[{Colors.Important}]Binary file.[/]\"))\n                else\n                    renderInlineDiff fileDiff.InlineDiff\n        else\n            addToOutput (Markup($\"[{Colors.Highlighted}]No differences found.[/]\"))\n\n    /// Creates the text output for a diff to the most recent specific ReferenceType.\n    type GetDiffByReferenceTypeParameters() =\n        member val public BranchId = String.Empty with get, set\n        member val public BranchName = BranchName String.Empty with get, set\n\n    let private diffToReferenceType (parseResult: ParseResult) (referenceType: ReferenceType) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n\n            let validateIncomingParameters = parseResult |> Validations.CommonValidations\n\n            match validateIncomingParameters with\n            | Ok _ ->\n                let graceIds = getNormalizedIdsAndNames parseResult\n\n                if parseResult |> hasOutput then\n                    do!\n                        progress\n                            .Columns(progressColumns)\n                            .StartAsync(fun progressContext ->\n                                task {\n                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace index file.[/]\")\n\n                                    let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Scanning working directory for changes.[/]\", autoStart = false)\n\n                                    let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating new directory verions.[/]\", autoStart = false)\n\n                                    let t3 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading changed files to object storage.[/]\", autoStart = false)\n\n                                    let t4 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading new directory versions.[/]\", autoStart = false)\n\n                                    let t5 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating a save reference.[/]\", autoStart = false)\n\n                                    let t6 =\n                                        progressContext.AddTask(\n                                            $\"[{Color.DodgerBlue1}]Getting {(getDiscriminatedUnionCaseName referenceType)\n                                                                                .ToLowerInvariant()}.[/]\",\n                                            autoStart = false\n                                        )\n\n                                    let t7 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending diff request to server.[/]\", autoStart = false)\n\n                                    let mutable rootDirectoryId = DirectoryVersionId.Empty\n                                    let mutable rootDirectorySha256Hash = Sha256Hash String.Empty\n                                    let mutable previousDirectoryIds: HashSet<DirectoryVersionId> = null\n\n                                    // Check for latest commit and latest root directory version from grace watch. If it's running, we know GraceStatus is up-to-date.\n                                    match! getGraceWatchStatus () with\n                                    | Some graceWatchStatus ->\n                                        t0.Value <- 100.0\n                                        t1.Value <- 100.0\n                                        t2.Value <- 100.0\n                                        t3.Value <- 100.0\n                                        t4.Value <- 100.0\n                                        t5.Value <- 100.0\n                                        rootDirectoryId <- graceWatchStatus.RootDirectoryId\n                                        rootDirectorySha256Hash <- graceWatchStatus.RootDirectorySha256Hash\n                                        previousDirectoryIds <- graceWatchStatus.DirectoryIds\n                                    | None ->\n                                        let! previousGraceStatus = readGraceStatusFile ()\n                                        t0.Value <- 100.0\n                                        t1.StartTask()\n                                        let! differences = scanForDifferences previousGraceStatus\n                                        let! newFileVersions = copyUpdatedFilesToObjectCache t1 differences\n                                        t1.Value <- 100.0\n\n                                        t2.StartTask()\n\n                                        let! (updatedGraceStatus, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                                        do! applyGraceStatusIncremental updatedGraceStatus newDirectoryVersions differences\n                                        rootDirectoryId <- updatedGraceStatus.RootDirectoryId\n                                        rootDirectorySha256Hash <- updatedGraceStatus.RootDirectorySha256Hash\n                                        previousDirectoryIds <- updatedGraceStatus.Index.Keys.ToHashSet()\n                                        t2.Value <- 100.0\n\n                                        t3.StartTask()\n\n                                        let getUploadMetadataForFilesParameters =\n                                            GetUploadMetadataForFilesParameters(\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                CorrelationId = getCorrelationId parseResult,\n                                                FileVersions =\n                                                    (newFileVersions\n                                                     |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                                     |> Seq.toArray)\n                                            )\n\n                                        match! uploadFilesToObjectStorage getUploadMetadataForFilesParameters with\n                                        | Ok returnValue -> ()\n                                        | Error error -> logToAnsiConsole Colors.Error $\"Failed to upload changed files to object storage. {error}\"\n\n                                        t3.Value <- 100.0\n\n                                        t4.StartTask()\n\n                                        if (newDirectoryVersions.Count > 0) then\n                                            (task {\n                                                let saveParameters = SaveDirectoryVersionsParameters()\n                                                saveParameters.OwnerId <- graceIds.OwnerIdString\n                                                saveParameters.OwnerName <- graceIds.OwnerName\n                                                saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                                                saveParameters.OrganizationName <- graceIds.OrganizationName\n                                                saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                                                saveParameters.RepositoryName <- graceIds.RepositoryName\n                                                saveParameters.CorrelationId <- getCorrelationId parseResult\n\n                                                saveParameters.DirectoryVersions <-\n                                                    newDirectoryVersions\n                                                        .Select(fun dv -> dv.ToDirectoryVersion)\n                                                        .ToList()\n\n                                                match! DirectoryVersion.SaveDirectoryVersions saveParameters with\n                                                | Ok returnValue -> ()\n                                                | Error error -> logToAnsiConsole Colors.Error $\"Failed to upload new directory versions. {error}\"\n                                            })\n                                                .Wait()\n\n                                        t4.Value <- 100.0\n\n                                        t5.StartTask()\n\n                                        if newDirectoryVersions.Count > 0 then\n                                            (task {\n                                                match!\n                                                    createSaveReference\n                                                        (getRootDirectoryVersion updatedGraceStatus)\n                                                        $\"Created during `grace diff {(getDiscriminatedUnionCaseName referenceType)\n                                                                                          .ToLowerInvariant()}` operation.\"\n                                                        (getCorrelationId parseResult)\n                                                    with\n                                                | Ok saveReference -> ()\n                                                | Error error -> logToAnsiConsole Colors.Error $\"Failed to create a save reference. {error}\"\n                                            })\n                                                .Wait()\n\n                                        t5.Value <- 100.0\n\n                                    // Check for latest reference of the given type from the server.\n                                    t6.StartTask()\n\n                                    let getReferencesParameters =\n                                        GetReferencesParameters(\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OwnerName = graceIds.OwnerName,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            OrganizationName = graceIds.OrganizationName,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            RepositoryName = graceIds.RepositoryName,\n                                            BranchId = graceIds.BranchIdString,\n                                            BranchName = graceIds.BranchName,\n                                            MaxCount = 1,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    let! getAReferenceResult =\n                                        task {\n                                            match referenceType with\n                                            | Commit -> return! Branch.GetCommits getReferencesParameters\n                                            | Checkpoint -> return! Branch.GetCheckpoints getReferencesParameters\n                                            | Save -> return! Branch.GetSaves getReferencesParameters\n                                            | Tag -> return! Branch.GetTags getReferencesParameters\n                                            | External -> return! Branch.GetExternals getReferencesParameters\n                                            | Rebase -> return! Branch.GetRebases getReferencesParameters\n\n                                            // Promotions are different, because we actually want the promotion from the parent branch that this branch is based on.\n                                            | Promotion ->\n                                                let promotions = List<ReferenceDto>()\n\n                                                let branchParameters =\n                                                    Parameters.Branch.GetBranchParameters(\n                                                        OwnerId = graceIds.OwnerIdString,\n                                                        OwnerName = graceIds.OwnerName,\n                                                        OrganizationId = graceIds.OrganizationIdString,\n                                                        OrganizationName = graceIds.OrganizationName,\n                                                        RepositoryId = graceIds.RepositoryIdString,\n                                                        RepositoryName = graceIds.RepositoryName,\n                                                        BranchId = graceIds.BranchIdString,\n                                                        BranchName = graceIds.BranchName,\n                                                        CorrelationId = graceIds.CorrelationId\n                                                    )\n\n                                                match! Branch.Get(branchParameters) with\n                                                | Ok returnValue ->\n                                                    let branchDto = returnValue.ReturnValue\n                                                    promotions.Add(branchDto.BasedOn)\n                                                | Error error ->\n                                                    logToAnsiConsole Colors.Error (Markup.Escape($\"Error in Branch.Get: {error}\"))\n\n                                                    if parseResult |> json || parseResult |> verbose then\n                                                        logToAnsiConsole Colors.Verbose (serialize error)\n\n                                                return Ok(GraceReturnValue.Create (promotions.ToArray()) graceIds.CorrelationId)\n                                        }\n\n                                    let latestReference =\n                                        match getAReferenceResult with\n                                        | Ok returnValue ->\n                                            // There should only be one reference, because we're using MaxCount = 1.\n                                            let references = returnValue.ReturnValue\n\n                                            if references.Count() > 0 then\n                                                //logToAnsiConsole Colors.Verbose $\"Got latest reference: {references.First().ReferenceText}; {references.First().CreatedAt}; {getShortenedSha256Hash (references.First().Sha256Hash)}; {references.First().DirectoryId}.\"\n                                                references.First()\n                                            else\n                                                logToAnsiConsole Colors.Error $\"Error getting latest reference. No matching references were found.\"\n\n                                                ReferenceDto.Default\n                                        | Error error ->\n                                            logToAnsiConsole Colors.Error $\"Error getting latest reference: {Markup.Escape(error.Error)}.\"\n\n                                            ReferenceDto.Default\n\n                                    t6.Value <- 100.0\n\n                                    // Sending diff request to server.\n                                    t7.StartTask()\n                                    //logToAnsiConsole Colors.Verbose $\"latestReference.DirectoryId: {latestReference.DirectoryId}; rootDirectoryId: {rootDirectoryId}.\"\n                                    let getDiffParameters =\n                                        GetDiffParameters(\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OwnerName = graceIds.OwnerName,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            OrganizationName = graceIds.OrganizationName,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            RepositoryName = graceIds.RepositoryName,\n                                            DirectoryVersionId1 = latestReference.DirectoryId,\n                                            DirectoryVersionId2 = rootDirectoryId,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    let! getDiffResult = Diff.GetDiff(getDiffParameters)\n\n                                    match getDiffResult with\n                                    | Ok returnValue ->\n                                        let diffDto = returnValue.ReturnValue\n                                        printDiffResults diffDto\n                                    | Error error ->\n                                        let s = StringExtensions.EscapeMarkup($\"{error.Error}\")\n                                        logToAnsiConsole Colors.Error $\"Error submitting diff: {s}\"\n\n                                        if parseResult |> json || parseResult |> verbose then\n                                            logToAnsiConsole Colors.Verbose (serialize error)\n\n                                    t7.Increment(100.0)\n                                //AnsiConsole.MarkupLine($\"[{Colors.Important}]Differences: {differences.Count}.[/]\")\n                                //AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error.EscapeMarkup()}[/]\")\n                                })\n\n                    for markup in markupList do\n                        writeMarkup markup\n\n                    return 0\n                else\n                    // Do the thing here\n                    return 0\n            | Error error -> return (Error error) |> renderOutput parseResult\n        }\n\n    type PromotionHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task { return! diffToReferenceType parseResult ReferenceType.Promotion }\n\n    type CommitHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task { return! diffToReferenceType parseResult ReferenceType.Commit }\n\n    type CheckpointHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task { return! diffToReferenceType parseResult ReferenceType.Checkpoint }\n\n\n    type SaveHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task { return! diffToReferenceType parseResult ReferenceType.Save }\n\n    type TagHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task { return! diffToReferenceType parseResult ReferenceType.Tag }\n\n    type DirectoryIdHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> Validations.CommonValidations\n\n                match validateIncomingParameters with\n                | Ok _ -> return 0\n                | Error error -> return (Error error) |> renderOutput parseResult\n            }\n\n    type ShaHandler() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> Validations.CommonValidations\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    let graceIds = getNormalizedIdsAndNames parseResult\n\n                    if parseResult |> hasOutput then\n                        do!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace index file.[/]\")\n\n                                        let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Scanning working directory for changes.[/]\", autoStart = false)\n\n                                        let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating new directory verions.[/]\", autoStart = false)\n\n                                        let t3 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading changed files to object storage.[/]\", autoStart = false)\n\n                                        let t4 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading new directory versions.[/]\", autoStart = false)\n\n                                        let t5 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating a save reference.[/]\", autoStart = false)\n\n                                        let t6 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending diff request to server.[/]\", autoStart = false)\n\n                                        let mutable rootDirectoryId = DirectoryVersionId.Empty\n                                        let mutable rootDirectorySha256Hash = Sha256Hash String.Empty\n                                        let mutable previousDirectoryIds: HashSet<DirectoryVersionId> = null\n\n                                        // Check for latest commit and latest root directory version from grace watch. If it's running, we know GraceStatus is up-to-date.\n                                        match! getGraceWatchStatus () with\n                                        | Some graceWatchStatus ->\n                                            t0.Value <- 100.0\n                                            t1.Value <- 100.0\n                                            t2.Value <- 100.0\n                                            t3.Value <- 100.0\n                                            t4.Value <- 100.0\n                                            t5.Value <- 100.0\n                                            rootDirectoryId <- graceWatchStatus.RootDirectoryId\n                                            rootDirectorySha256Hash <- graceWatchStatus.RootDirectorySha256Hash\n                                            previousDirectoryIds <- graceWatchStatus.DirectoryIds\n                                        | None ->\n                                            let! previousGraceStatus = readGraceStatusFile ()\n                                            let mutable graceStatus = previousGraceStatus\n                                            t0.Value <- 100.0\n                                            t1.StartTask()\n                                            let! differences = scanForDifferences previousGraceStatus\n                                            let! newFileVersions = copyUpdatedFilesToObjectCache t1 differences\n                                            t1.Value <- 100.0\n\n                                            t2.StartTask()\n\n                                            let! (updatedGraceStatus, newDirectoryVersions) =\n                                                getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                                            do! applyGraceStatusIncremental updatedGraceStatus newDirectoryVersions differences\n                                            rootDirectoryId <- updatedGraceStatus.RootDirectoryId\n                                            rootDirectorySha256Hash <- updatedGraceStatus.RootDirectorySha256Hash\n                                            previousDirectoryIds <- updatedGraceStatus.Index.Keys.ToHashSet()\n                                            t2.Value <- 100.0\n\n                                            t3.StartTask()\n\n                                            let getUploadMetadataForFilesParameters =\n                                                GetUploadMetadataForFilesParameters(\n                                                    OwnerId = graceIds.OwnerIdString,\n                                                    OwnerName = graceIds.OwnerName,\n                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                    OrganizationName = graceIds.OrganizationName,\n                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                    RepositoryName = graceIds.RepositoryName,\n                                                    CorrelationId = getCorrelationId parseResult,\n                                                    FileVersions =\n                                                        (newFileVersions\n                                                         |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                                         |> Seq.toArray)\n                                                )\n\n                                            match! uploadFilesToObjectStorage getUploadMetadataForFilesParameters with\n                                            | Ok returnValue -> ()\n                                            | Error error -> logToAnsiConsole Colors.Error $\"Failed to upload changed files to object storage. {error}\"\n\n                                            t3.Value <- 100.0\n\n                                            t4.StartTask()\n\n                                            if (newDirectoryVersions.Count > 0) then\n                                                (task {\n                                                    let saveParameters = SaveDirectoryVersionsParameters()\n                                                    saveParameters.OwnerId <- graceIds.OwnerIdString\n                                                    saveParameters.OwnerName <- graceIds.OwnerName\n                                                    saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                                                    saveParameters.OrganizationName <- graceIds.OrganizationName\n                                                    saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                                                    saveParameters.RepositoryName <- graceIds.RepositoryName\n                                                    saveParameters.CorrelationId <- getCorrelationId parseResult\n\n                                                    saveParameters.DirectoryVersions <-\n                                                        newDirectoryVersions\n                                                            .Select(fun dv -> dv.ToDirectoryVersion)\n                                                            .ToList()\n\n                                                    match! DirectoryVersion.SaveDirectoryVersions saveParameters with\n                                                    | Ok returnValue -> ()\n                                                    | Error error -> logToAnsiConsole Colors.Error $\"Failed to upload new directory versions. {error}\"\n                                                })\n                                                    .Wait()\n\n                                            t4.Value <- 100.0\n\n                                            t5.StartTask()\n\n                                            if newDirectoryVersions.Count > 0 then\n                                                (task {\n                                                    match!\n                                                        createSaveReference\n                                                            (getRootDirectoryVersion updatedGraceStatus)\n                                                            $\"Created during `grace diff sha` operation.\"\n                                                            (getCorrelationId parseResult)\n                                                        with\n                                                    | Ok saveReference -> ()\n                                                    | Error error -> logToAnsiConsole Colors.Error $\"Failed to create a save reference. {error}\"\n                                                })\n                                                    .Wait()\n\n                                            t5.Value <- 100.0\n\n                                        // Check for latest reference of the given type from the server.\n                                        t6.StartTask()\n\n                                        let sha256Hash1 = parseResult.GetValue(Options.sha256Hash1)\n                                        let sha256Hash2 = parseResult.GetValue(Options.sha256Hash2)\n\n                                        let getDiffBySha256HashParameters =\n                                            GetDiffBySha256HashParameters(\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                Sha256Hash1 = sha256Hash1,\n                                                Sha256Hash2 = sha256Hash2,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        match! Diff.GetDiffBySha256Hash(getDiffBySha256HashParameters) with\n                                        | Ok returnValue ->\n                                            let diffDto = returnValue.ReturnValue\n                                            printDiffResults diffDto\n                                            t6.Value <- 100.0\n                                        | Error error -> logToAnsiConsole Colors.Error $\"Failed to get diff by sha256 hash. {error}\"\n\n                                        t6.Value <- 100.0\n                                    })\n\n                        for markup in markupList do\n                            writeMarkup markup\n\n                        return 0\n                    else\n                        // Do the thing here\n                        return 0\n                | Error error -> return (Error error) |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let addBranchOptions (command: Command) =\n            command\n            |> addOption Options.branchName\n            |> addOption Options.branchId\n\n        let diffCommand = new Command(\"diff\", Description = \"Displays the difference between two versions of your repository.\")\n\n        let promotionCommand =\n            new Command(\"promotion\", Description = \"Displays the difference between the promotion that this branch is based on and your current version.\")\n            |> addCommonOptions\n            |> addBranchOptions\n\n        promotionCommand.Action <- PromotionHandler()\n        diffCommand.Subcommands.Add(promotionCommand)\n\n        let commitCommand =\n            new Command(\"commit\", Description = \"Displays the difference between the most recent commit and your current version.\")\n            |> addCommonOptions\n            |> addBranchOptions\n\n        commitCommand.Action <- CommitHandler()\n        diffCommand.Subcommands.Add(commitCommand)\n\n        let checkpointCommand =\n            new Command(\"checkpoint\", Description = \"Displays the difference between the most recent checkpoint and your current version.\")\n            |> addCommonOptions\n            |> addBranchOptions\n\n        checkpointCommand.Action <- CheckpointHandler()\n        diffCommand.Subcommands.Add(checkpointCommand)\n\n        let saveCommand =\n            new Command(\"save\", Description = \"Displays the difference between the most recent save and your current version.\")\n            |> addCommonOptions\n            |> addBranchOptions\n\n        saveCommand.Action <- SaveHandler()\n        diffCommand.Subcommands.Add(saveCommand)\n\n        let tagCommand =\n            new Command(\"tag\", Description = \"Displays the difference between the specified tag and your current version.\")\n            |> addCommonOptions\n            |> addBranchOptions\n            |> addOption Options.tag\n\n        tagCommand.Action <- TagHandler()\n        diffCommand.Subcommands.Add(tagCommand)\n\n        let directoryIdCommand =\n            new Command(\n                \"directoryid\",\n                Description =\n                    \"Displays the difference between two versions, specified by DirectoryId. If a second DirectoryId is not supplied, the current branch's root DirectoryId will be used.\"\n            )\n            |> addCommonOptions\n            |> addOption Options.directoryVersionId1\n            |> addOption Options.directoryVersionId2\n\n        directoryIdCommand.Action <- DirectoryIdHandler()\n        diffCommand.Subcommands.Add(directoryIdCommand)\n\n        let shaCommand =\n            new Command(\n                \"sha\",\n                Description =\n                    \"Displays the difference between two versions, specified by partial or full SHA-256 hash. If a second SHA-256 value is not supplied, the current branch's root SHA-256 hash will be used.\"\n            )\n            |> addCommonOptions\n            |> addOption Options.sha256Hash1\n            |> addOption Options.sha256Hash2\n\n        shaCommand.Action <- ShaHandler()\n        diffCommand.Subcommands.Add(shaCommand)\n\n        diffCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/DirectoryVersion.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Client.Theme\nopen Grace.Types.Branch\nopen Grace.Types.Reference\nopen Grace.Shared.Parameters.Branch\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Errors\nopen NodaTime\nopen NodaTime.TimeZones\nopen Spectre.Console\nopen Spectre.Console.Json\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Globalization\nopen System.IO\nopen System.IO.Enumeration\nopen System.Linq\nopen System.Security.Cryptography\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\nopen Grace.Shared.Parameters.Storage\n\nmodule DirectoryVersion =\n    open Grace.Shared.Validation.Common.Input\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let maxCount =\n            new Option<int>(\n                OptionName.MaxCount,\n                Required = false,\n                Description = \"The maximum number of results to return.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> 30)\n            )\n\n        let sha256Hash =\n            new Option<String>(\n                OptionName.Sha256Hash,\n                [||],\n                Required = false,\n                Description = \"The full or partial SHA-256 hash value of the version.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let includeDeleted =\n            new Option<bool>(OptionName.IncludeDeleted, [| \"-d\" |], Required = false, Description = \"Include deleted branches in the result. [default: false]\")\n\n        let directoryVersionIdRequired =\n            new Option<DirectoryVersionId>(\n                OptionName.DirectoryVersionId,\n                [| \"-v\" |],\n                Required = true,\n                Description = \"The DirectoryVersionId to act on <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    let private DirectoryVersionValidations parseResult = Ok parseResult\n\n    type GetZipFile() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: Threading.CancellationToken) : Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> CommonValidations\n                        >>= DirectoryVersionValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let graceIds = getNormalizedIdsAndNames parseResult\n                        let directoryVersionId = parseResult.GetValue(Options.directoryVersionIdRequired)\n                        let sha256Hash = parseResult.GetValue(Options.sha256Hash)\n\n                        let sdkParameters =\n                            Parameters.DirectoryVersion.GetZipFileParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                DirectoryVersionId = $\"{directoryVersionId}\",\n                                Sha256Hash = sha256Hash,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = DirectoryVersion.GetZipFile(sdkParameters)\n                                            t0.Increment(100.0)\n\n                                            match result with\n                                            | Ok returnValue -> AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]{returnValue.ReturnValue}[/]\"\n                                            | Error error -> logToAnsiConsole Colors.Error $\"{error}\"\n\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = DirectoryVersion.GetZipFile(sdkParameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n                with\n                | ex ->\n                    return\n                        Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n                        |> renderOutput parseResult\n\n            //match result with\n            //| Ok returnValue -> AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]{returnValue.ReturnValue}[/]\"\n            //| Error error -> logToAnsiConsole Colors.Error $\"{error}\"\n\n            //return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n            |> addOption Options.directoryVersionIdRequired\n\n        // Create main command and aliases, if any.`\n        let directoryVersionCommand = new Command(\"directory-version\", Description = \"Work with directory versions in a repository.\")\n\n        directoryVersionCommand.Aliases.Add(\"dv\")\n        directoryVersionCommand.Aliases.Add(\"ver\")\n\n        let getZipFileCommand =\n            new Command(\"get-zip-file\", Description = \"Gets the .zip file for a specific directory version.\")\n            |> addOption Options.sha256Hash\n            |> addCommonOptions\n\n        getZipFileCommand.Action <- GetZipFile()\n        directoryVersionCommand.Subcommands.Add(getZipFileCommand)\n\n        directoryVersionCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/History.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI\nopen Grace.CLI.Common\nopen Grace.CLI.Text\nopen Grace.Shared\nopen Grace.Shared.Client\nopen Grace.Shared.Utilities\nopen NodaTime\nopen Spectre.Console\nopen System\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Diagnostics\nopen System.IO\nopen System.Linq\nopen System.Threading.Tasks\n\nmodule History =\n\n    module private Options =\n        let limit =\n            new Option<int>(\n                OptionName.Limit,\n                Required = false,\n                Description = \"Maximum number of history entries to show.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> 50)\n            )\n\n        let repo =\n            new Option<bool>(\n                OptionName.Repo,\n                Required = false,\n                Description = \"Filter to entries whose repoRoot matches the current directory.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let failed =\n            new Option<bool>(\n                OptionName.Failed,\n                Required = false,\n                Description = \"Show only failed commands.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let success =\n            new Option<bool>(\n                OptionName.Success,\n                Required = false,\n                Description = \"Show only successful commands.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let since =\n            new Option<string>(\n                OptionName.Since,\n                Required = false,\n                Description = \"Only show entries since a duration (e.g. 10m, 24h, 7d).\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let contains =\n            new Option<string>(\n                OptionName.Contains,\n                Required = false,\n                Description = \"Substring match against the stored command line.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let showId =\n            new Option<bool>(\n                OptionName.Id,\n                Required = false,\n                Description = \"Show the stable id column.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let runNumber = new Argument<int>(\"number\", Description = \"History number (from most recent = 1).\", Arity = ArgumentArity.ZeroOrOne)\n\n        let runId =\n            new Option<Guid>(\n                OptionName.Id,\n                Required = false,\n                Description = \"Run by stable id instead of history number.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> Guid.Empty)\n            )\n\n        let yes =\n            new Option<bool>(\n                OptionName.Yes,\n                Required = false,\n                Description = \"Skip confirmation prompts.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let useCurrentCwd =\n            new Option<bool>(\n                OptionName.UseCurrentCwd,\n                Required = false,\n                Description = \"Run from the current working directory instead of the recorded one.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let dryRun =\n            new Option<bool>(\n                OptionName.DryRun,\n                Required = false,\n                Description = \"Print the resolved command and working directory without executing.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let replace =\n            new Option<string []>(\n                OptionName.Replace,\n                Required = false,\n                Description = \"Provide replacements for redacted values (name=value or argIndex=value).\",\n                Arity = ArgumentArity.OneOrMore\n            )\n\n    let private warnOnCorruptConfig (parseResult: ParseResult) (loadResult: UserConfiguration.UserConfigurationLoadResult) =\n        if\n            loadResult.WasCorrupt && not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let message =\n                match loadResult.ErrorMessage with\n                | Some error -> error\n                | None -> \"User configuration is invalid; using defaults.\"\n\n            AnsiConsole.MarkupLine($\"[yellow]{Markup.Escape(message)}[/]\")\n\n    let private formatDuration (durationMs: int64) = $\"{(float durationMs) / 1000.0:F3}s\"\n\n    let private abbreviatePath (value: string) =\n        if String.IsNullOrWhiteSpace(value) then String.Empty\n        elif value.Length <= 40 then value\n        else \"...\" + value.Substring(value.Length - 37)\n\n    let private formatRepoName (entry: HistoryStorage.HistoryEntry) =\n        match entry.repoName with\n        | Some name -> name\n        | None ->\n            match entry.repoRoot with\n            | Some root ->\n                try\n                    let name = DirectoryInfo(root).Name\n                    if String.IsNullOrWhiteSpace(name) then String.Empty else name\n                with\n                | _ -> String.Empty\n            | None -> String.Empty\n\n    let private formatRepoBranch (entry: HistoryStorage.HistoryEntry) =\n        entry.repoBranch\n        |> Option.defaultValue String.Empty\n\n    let internal filterEntries\n        (entries: HistoryStorage.HistoryEntry list)\n        (limit: int)\n        (filterRepo: bool)\n        (filterFailed: bool)\n        (filterSuccess: bool)\n        (sinceDuration: Duration option)\n        (containsText: string option)\n        (sourceText: string option)\n        =\n        let normalizedContainsText =\n            containsText\n            |> Option.bind (fun text -> if String.IsNullOrWhiteSpace(text) then None else Some(text.Trim()))\n\n        let normalizedSourceText =\n            sourceText\n            |> Option.bind (fun source -> if String.IsNullOrWhiteSpace(source) then None else Some(source.Trim()))\n\n        let mutable filtered = entries\n\n        if filterRepo then\n            match HistoryStorage.tryFindRepoRoot Environment.CurrentDirectory with\n            | Some repoRoot ->\n                let comparer =\n                    if runningOnWindows then\n                        StringComparer.InvariantCultureIgnoreCase\n                    else\n                        StringComparer.InvariantCulture\n\n                filtered <-\n                    filtered\n                    |> List.filter (fun entry ->\n                        match entry.repoRoot with\n                        | Some entryRoot -> comparer.Equals(entryRoot, repoRoot)\n                        | None -> false)\n            | None -> filtered <- List.empty\n\n        match sinceDuration with\n        | Some duration ->\n            let cutoff = getCurrentInstant().Minus(duration)\n\n            filtered <-\n                filtered\n                |> List.filter (fun entry -> entry.timestampUtc >= cutoff)\n        | None -> ()\n\n        match normalizedContainsText with\n        | Some text ->\n            filtered <-\n                filtered\n                |> List.filter (fun entry ->\n                    entry.commandLine.IndexOf(text, StringComparison.InvariantCultureIgnoreCase)\n                    >= 0)\n        | None -> ()\n\n        match normalizedSourceText with\n        | Some source ->\n            filtered <-\n                filtered\n                |> List.filter (fun entry ->\n                    match entry.source with\n                    | Some entrySource -> entrySource.Equals(source, StringComparison.OrdinalIgnoreCase)\n                    | None -> false)\n        | None -> ()\n\n        if filterFailed && not filterSuccess then\n            filtered <-\n                filtered\n                |> List.filter (fun entry -> entry.exitCode <> 0)\n        elif filterSuccess && not filterFailed then\n            filtered <-\n                filtered\n                |> List.filter (fun entry -> entry.exitCode = 0)\n\n        let ordered =\n            filtered\n            |> List.sortByDescending (fun entry -> entry.timestampUtc)\n\n        if limit > 0 then\n            ordered\n            |> List.truncate (min limit ordered.Length)\n        else\n            ordered\n\n    let private renderTable (entries: HistoryStorage.HistoryEntry list) (showId: bool) =\n        let table = Table(Border = TableBorder.DoubleEdge, ShowHeaders = true)\n\n        let columns =\n            if showId then\n                [|\n                    TableColumn($\"[bold]#[/]\")\n                    TableColumn($\"[bold]When[/]\")\n                    TableColumn($\"[bold]Exit[/]\")\n                    TableColumn($\"[bold]Dur[/]\")\n                    TableColumn($\"[bold]Cwd[/]\")\n                    TableColumn($\"[bold]Repo[/]\")\n                    TableColumn($\"[bold]Branch[/]\")\n                    TableColumn($\"[bold]Command[/]\")\n                    TableColumn($\"[bold]Id[/]\")\n                |]\n            else\n                [|\n                    TableColumn($\"[bold]#[/]\")\n                    TableColumn($\"[bold]When[/]\")\n                    TableColumn($\"[bold]Exit[/]\")\n                    TableColumn($\"[bold]Dur[/]\")\n                    TableColumn($\"[bold]Cwd[/]\")\n                    TableColumn($\"[bold]Repo[/]\")\n                    TableColumn($\"[bold]Branch[/]\")\n                    TableColumn($\"[bold]Command[/]\")\n                |]\n\n        table.AddColumns(columns) |> ignore\n\n        entries\n        |> List.iteri (fun index entry ->\n            let row =\n                if showId then\n                    [|\n                        $\"{index + 1}\"\n                        Markup.Escape(ago entry.timestampUtc)\n                        $\"{entry.exitCode}\"\n                        formatDuration entry.durationMs\n                        Markup.Escape(abbreviatePath entry.cwd)\n                        Markup.Escape(formatRepoName entry)\n                        Markup.Escape(formatRepoBranch entry)\n                        Markup.Escape(entry.commandLine)\n                        $\"{entry.id}\"\n                    |]\n                else\n                    [|\n                        $\"{index + 1}\"\n                        Markup.Escape(ago entry.timestampUtc)\n                        $\"{entry.exitCode}\"\n                        formatDuration entry.durationMs\n                        Markup.Escape(abbreviatePath entry.cwd)\n                        Markup.Escape(formatRepoName entry)\n                        Markup.Escape(formatRepoBranch entry)\n                        Markup.Escape(entry.commandLine)\n                    |]\n\n            table.AddRow(row) |> ignore)\n\n        AnsiConsole.Write(table)\n\n    let private outputEntries (parseResult: ParseResult) (entries: HistoryStorage.HistoryEntry list) (showId: bool) (corruptCount: int) =\n        if parseResult |> json then\n            let payload = serialize entries\n            AnsiConsole.WriteLine(Markup.Escape(payload))\n        elif parseResult |> silent then\n            ()\n        else\n            if corruptCount > 0 then\n                AnsiConsole.MarkupLine($\"[yellow]Skipped {corruptCount} corrupt history entries.[/]\")\n\n            renderTable entries showId\n            AnsiConsole.WriteLine()\n\n    type HistoryOn() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: Threading.CancellationToken) : Task<int> =\n            task {\n                let loadResult = UserConfiguration.loadUserConfiguration ()\n                warnOnCorruptConfig parseResult loadResult\n\n                let configuration =\n                    if loadResult.WasCorrupt then\n                        UserConfiguration.UserConfiguration()\n                    else\n                        loadResult.Configuration\n\n                configuration.History.Enabled <- true\n\n                match UserConfiguration.saveUserConfiguration configuration with\n                | Ok _ ->\n                    if parseResult |> json then\n                        AnsiConsole.WriteLine(Markup.Escape(serialize {| enabled = true |}))\n                    elif parseResult |> silent then\n                        ()\n                    else\n                        AnsiConsole.MarkupLine(\"[green]History recording enabled.[/]\")\n\n                    return 0\n                | Error error ->\n                    if not (parseResult |> silent) then\n                        AnsiConsole.MarkupLine($\"[red]{Markup.Escape(error)}[/]\")\n\n                    return -1\n            }\n\n    type HistoryOff() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: Threading.CancellationToken) : Task<int> =\n            task {\n                let loadResult = UserConfiguration.loadUserConfiguration ()\n                warnOnCorruptConfig parseResult loadResult\n\n                let configuration =\n                    if loadResult.WasCorrupt then\n                        UserConfiguration.UserConfiguration()\n                    else\n                        loadResult.Configuration\n\n                configuration.History.Enabled <- false\n\n                match UserConfiguration.saveUserConfiguration configuration with\n                | Ok _ ->\n                    if parseResult |> json then\n                        AnsiConsole.WriteLine(Markup.Escape(serialize {| enabled = false |}))\n                    elif parseResult |> silent then\n                        ()\n                    else\n                        AnsiConsole.MarkupLine(\"[green]History recording disabled.[/]\")\n\n                    return 0\n                | Error error ->\n                    if not (parseResult |> silent) then\n                        AnsiConsole.MarkupLine($\"[red]{Markup.Escape(error)}[/]\")\n\n                    return -1\n            }\n\n    type HistoryShow() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: Threading.CancellationToken) : Task<int> =\n            task {\n                let limit = parseResult.GetValue(Options.limit)\n                let filterRepo = parseResult.GetValue(Options.repo)\n                let filterFailed = parseResult.GetValue(Options.failed)\n                let filterSuccess = parseResult.GetValue(Options.success)\n                let sinceText = parseResult.GetValue(Options.since)\n                let containsText = parseResult.GetValue(Options.contains)\n                let sourceText = parseResult.GetValue(Common.Options.source)\n                let showId = parseResult.GetValue(Options.showId)\n\n                if limit < 0 then\n                    AnsiConsole.MarkupLine(\"[red]Limit must be positive.[/]\")\n                    return -1\n                else\n                    let sinceDuration =\n                        if String.IsNullOrWhiteSpace(sinceText) then\n                            Ok None\n                        else\n                            match HistoryStorage.tryParseDuration sinceText with\n                            | Ok duration -> Ok(Some duration)\n                            | Error error -> Error error\n\n                    match sinceDuration with\n                    | Error error ->\n                        AnsiConsole.MarkupLine($\"[red]{Markup.Escape(error)}[/]\")\n                        return -1\n                    | Ok since ->\n                        let readResult = HistoryStorage.readHistoryEntries ()\n\n                        let filtered =\n                            filterEntries\n                                readResult.Entries\n                                limit\n                                filterRepo\n                                filterFailed\n                                filterSuccess\n                                since\n                                (if String.IsNullOrWhiteSpace(containsText) then None else Some containsText)\n                                (if String.IsNullOrWhiteSpace(sourceText) then None else Some sourceText)\n\n                        outputEntries parseResult filtered showId readResult.CorruptCount\n                        return 0\n            }\n\n    type HistorySearch() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: Threading.CancellationToken) : Task<int> =\n            task {\n                let searchText = parseResult.GetValue<string>(\"text\")\n                let limit = parseResult.GetValue(Options.limit)\n                let filterRepo = parseResult.GetValue(Options.repo)\n                let filterFailed = parseResult.GetValue(Options.failed)\n                let filterSuccess = parseResult.GetValue(Options.success)\n                let sinceText = parseResult.GetValue(Options.since)\n                let sourceText = parseResult.GetValue(Common.Options.source)\n                let showId = parseResult.GetValue(Options.showId)\n\n                if limit < 0 then\n                    AnsiConsole.MarkupLine(\"[red]Limit must be positive.[/]\")\n                    return -1\n                else\n                    let sinceDuration =\n                        if String.IsNullOrWhiteSpace(sinceText) then\n                            Ok None\n                        else\n                            match HistoryStorage.tryParseDuration sinceText with\n                            | Ok duration -> Ok(Some duration)\n                            | Error error -> Error error\n\n                    match sinceDuration with\n                    | Error error ->\n                        AnsiConsole.MarkupLine($\"[red]{Markup.Escape(error)}[/]\")\n                        return -1\n                    | Ok since ->\n                        let readResult = HistoryStorage.readHistoryEntries ()\n\n                        let filtered =\n                            filterEntries\n                                readResult.Entries\n                                limit\n                                filterRepo\n                                filterFailed\n                                filterSuccess\n                                since\n                                (if String.IsNullOrWhiteSpace(searchText) then None else Some searchText)\n                                (if String.IsNullOrWhiteSpace(sourceText) then None else Some sourceText)\n\n                        outputEntries parseResult filtered showId readResult.CorruptCount\n                        return 0\n            }\n\n    let private parseReplacements (values: string array) =\n        let replacements = Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)\n        let errors = ResizeArray<string>()\n\n        if isNull values then\n            Ok replacements\n        else\n            for value in values do\n                let trimmed = if isNull value then String.Empty else value.Trim()\n                let equalsIndex = trimmed.IndexOf('=')\n\n                if equalsIndex <= 0 then\n                    errors.Add($\"Invalid replacement '{trimmed}'. Expected name=value.\")\n                else\n                    let name = trimmed.Substring(0, equalsIndex).Trim()\n                    let replacement = trimmed.Substring(equalsIndex + 1)\n\n                    if String.IsNullOrWhiteSpace(name) then\n                        errors.Add($\"Invalid replacement '{trimmed}'. Name cannot be empty.\")\n                    else\n                        replacements[name] <- replacement\n\n            if errors.Count > 0 then\n                Error(String.Join(Environment.NewLine, errors))\n            else\n                Ok replacements\n\n    let private replaceFirst (text: string) (replacement: string) =\n        let index = text.IndexOf(HistoryStorage.Placeholder, StringComparison.Ordinal)\n\n        if index < 0 then\n            text\n        else\n            text.Substring(0, index)\n            + replacement\n            + text.Substring(index + HistoryStorage.Placeholder.Length)\n\n    let private applyReplacements (argv: string array) (redactions: HistoryStorage.Redaction list) (replacements: IDictionary<string, string>) =\n        let updated = Array.copy argv\n        let missing = ResizeArray<HistoryStorage.Redaction>()\n\n        for redaction in redactions do\n            let indexKey = $\"{redaction.argIndex}\"\n\n            let replacement =\n                if replacements.ContainsKey(indexKey) then\n                    Some replacements[indexKey]\n                elif replacements.ContainsKey(redaction.name) then\n                    Some replacements[redaction.name]\n                else\n                    None\n\n            match replacement with\n            | Some value ->\n                let currentArg = updated[redaction.argIndex]\n                updated[redaction.argIndex] <- replaceFirst currentArg value\n            | None -> missing.Add(redaction)\n\n        updated, missing |> Seq.toList\n\n    let private promptForReplacements (missing: HistoryStorage.Redaction list) (replacements: IDictionary<string, string>) =\n        for redaction in missing do\n            let key = $\"{redaction.argIndex}\"\n\n            if not <| replacements.ContainsKey(key) then\n                let prompt =\n                    TextPrompt<string>($\"Replacement for {redaction.name} (arg #{redaction.argIndex + 1}):\")\n                        .Secret()\n\n                let value = AnsiConsole.Prompt(prompt)\n                replacements[key] <- value\n\n    let private resolveWorkingDirectory (entryCwd: string) (useCurrentCwd: bool) (canPrompt: bool) (yes: bool) =\n        if useCurrentCwd then\n            Ok(Environment.CurrentDirectory, false)\n        elif String.IsNullOrWhiteSpace(entryCwd) then\n            Error \"History entry did not record a working directory. Use --use-current-cwd to run from the current directory.\"\n        elif Directory.Exists(entryCwd) then\n            Ok(entryCwd, false)\n        else if canPrompt && not yes then\n            let message = $\"Recorded working directory not found: {Markup.Escape(entryCwd)}. Use current directory instead?\"\n\n            let useCurrent = AnsiConsole.Confirm(message, defaultValue = true)\n\n            if useCurrent then\n                Ok(Environment.CurrentDirectory, true)\n            else\n                Error $\"Recorded working directory not found: {entryCwd}.\"\n        else\n            Error $\"Recorded working directory not found: {entryCwd}. Use --use-current-cwd to run from the current directory.\"\n\n    type HistoryRun() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: Threading.CancellationToken) : Task<int> =\n            let number = parseResult.GetValue(Options.runNumber)\n            let byId = parseResult.GetValue(Options.runId)\n            let yes = parseResult.GetValue(Options.yes)\n            let useCurrentCwd = parseResult.GetValue(Options.useCurrentCwd)\n            let dryRun = parseResult.GetValue(Options.dryRun)\n            let replacementsInput = parseResult.GetValue(Options.replace)\n\n            let canPrompt =\n                not Console.IsInputRedirected\n                && not Console.IsOutputRedirected\n\n            let loadResult = UserConfiguration.loadUserConfiguration ()\n            warnOnCorruptConfig parseResult loadResult\n            let historyConfig = loadResult.Configuration.History\n\n            let readResult = HistoryStorage.readHistoryEntries ()\n\n            let ordered =\n                readResult.Entries\n                |> List.sortByDescending (fun entry -> entry.timestampUtc)\n\n            let target =\n                if byId <> Guid.Empty then\n                    ordered\n                    |> List.tryFind (fun entry -> entry.id = byId)\n                else if number <= 0 || number > ordered.Length then\n                    None\n                else\n                    Some ordered[number - 1]\n\n            let exitCode =\n                match target with\n                | None ->\n                    AnsiConsole.MarkupLine(\"[red]History entry not found.[/]\")\n                    -1\n                | Some entry ->\n                    match parseReplacements replacementsInput with\n                    | Error error ->\n                        AnsiConsole.MarkupLine($\"[red]{Markup.Escape(error)}[/]\")\n                        -1\n                    | Ok replacements ->\n                        let mutable argvToRun = entry.argvNormalized\n                        let mutable missingRedactions = List.empty\n\n                        if entry.redactions.Length > 0 then\n                            let updated, missing = applyReplacements argvToRun entry.redactions replacements\n                            argvToRun <- updated\n                            missingRedactions <- missing\n\n                            if missingRedactions.Length > 0\n                               && canPrompt\n                               && not yes then\n                                promptForReplacements missingRedactions replacements\n                                let updatedAfterPrompt, missingAfterPrompt = applyReplacements argvToRun entry.redactions replacements\n\n                                argvToRun <- updatedAfterPrompt\n                                missingRedactions <- missingAfterPrompt\n\n                        let stillRedacted =\n                            argvToRun\n                            |> Array.exists (fun arg -> arg.Contains(HistoryStorage.Placeholder))\n\n                        if missingRedactions.Length > 0 || stillRedacted then\n                            let missingKeys =\n                                missingRedactions\n                                |> List.map (fun redaction -> $\"{redaction.name} (arg #{redaction.argIndex + 1})\")\n                                |> Seq.distinct\n                                |> String.concat \", \"\n\n                            AnsiConsole.MarkupLine($\"[red]Missing replacements for redacted values: {Markup.Escape(missingKeys)}[/]\")\n                            -1\n                        else\n                            match resolveWorkingDirectory entry.cwd useCurrentCwd canPrompt yes with\n                            | Error message ->\n                                AnsiConsole.MarkupLine($\"[red]{Markup.Escape(message)}[/]\")\n                                -1\n                            | Ok (cwd, usedFallback) ->\n                                if usedFallback && not (parseResult |> silent) then\n                                    AnsiConsole.MarkupLine($\"[yellow]Recorded working directory not found; using current directory: {Markup.Escape(cwd)}[/]\")\n\n                                let commandLine = HistoryStorage.buildCommandLine argvToRun\n\n                                AnsiConsole.MarkupLine($\"[bold]About to run:[/] grace {Markup.Escape(commandLine)}\")\n\n                                AnsiConsole.MarkupLine($\"[bold]Working directory:[/] {Markup.Escape(cwd)}\")\n\n                                let shouldProceed =\n                                    if HistoryStorage.isDestructive commandLine historyConfig\n                                       && not yes then\n                                        AnsiConsole.Confirm(\"This command looks destructive. Re-run?\", defaultValue = false)\n                                    else\n                                        true\n\n                                if not shouldProceed then\n                                    1\n                                else if dryRun then\n                                    0\n                                else\n                                    let executablePath = Environment.ProcessPath\n\n                                    if String.IsNullOrWhiteSpace(executablePath) then\n                                        AnsiConsole.MarkupLine(\"[red]Failed to locate Grace executable.[/]\")\n                                        -1\n                                    else\n                                        try\n                                            let startInfo = ProcessStartInfo()\n                                            startInfo.FileName <- executablePath\n                                            startInfo.WorkingDirectory <- cwd\n                                            startInfo.UseShellExecute <- false\n                                            startInfo.RedirectStandardInput <- false\n                                            startInfo.RedirectStandardOutput <- false\n                                            startInfo.RedirectStandardError <- false\n\n                                            argvToRun |> Array.iter startInfo.ArgumentList.Add\n\n                                            use proc = new Process()\n                                            proc.StartInfo <- startInfo\n\n                                            if proc.Start() then\n                                                proc.WaitForExit()\n                                                proc.ExitCode\n                                            else\n                                                AnsiConsole.MarkupLine(\"[red]Failed to start Grace process.[/]\")\n                                                -1\n                                        with\n                                        | ex ->\n                                            AnsiConsole.MarkupLine($\"[red]Failed to start Grace process: {Markup.Escape(ex.Message)}[/]\")\n                                            -1\n\n            Task.FromResult(exitCode)\n\n    type HistoryDelete() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: Threading.CancellationToken) : Task<int> =\n            task {\n                match HistoryStorage.clearHistory () with\n                | Ok removed ->\n                    if parseResult |> json then\n                        AnsiConsole.WriteLine(Markup.Escape(serialize {| deleted = true; removed = removed |}))\n                    elif parseResult |> silent then\n                        ()\n                    else\n                        AnsiConsole.MarkupLine(\"[green]History cleared.[/]\")\n\n                    return 0\n                | Error error ->\n                    if not (parseResult |> silent) then\n                        AnsiConsole.MarkupLine($\"[red]{Markup.Escape(error)}[/]\")\n\n                    return -1\n            }\n\n    let Build =\n        let historyCommand = Command(\"history\", Description = \"Manage local Grace CLI history.\")\n\n        let onCommand = Command(\"on\", Description = \"Enable history recording.\")\n        onCommand.Action <- HistoryOn()\n\n        let offCommand = Command(\"off\", Description = \"Disable history recording.\")\n        offCommand.Action <- HistoryOff()\n\n        let showCommand = Command(\"show\", Description = \"Show history entries.\")\n\n        showCommand\n        |> addOption Options.limit\n        |> addOption Options.repo\n        |> addOption Options.failed\n        |> addOption Options.success\n        |> addOption Options.since\n        |> addOption Options.contains\n        |> addOption Options.showId\n        |> ignore\n\n        showCommand.Action <- HistoryShow()\n\n        let searchCommand = Command(\"search\", Description = \"Search history entries.\")\n        let searchText = new Argument<string>(\"text\", Description = \"Text to search for.\")\n        searchCommand.Arguments.Add(searchText)\n\n        searchCommand\n        |> addOption Options.limit\n        |> addOption Options.repo\n        |> addOption Options.failed\n        |> addOption Options.success\n        |> addOption Options.since\n        |> addOption Options.showId\n        |> ignore\n\n        searchCommand.Action <- HistorySearch()\n\n        let runCommand = Command(\"run\", Description = \"Re-run a prior command.\")\n        runCommand.Arguments.Add(Options.runNumber)\n\n        runCommand\n        |> addOption Options.runId\n        |> addOption Options.yes\n        |> addOption Options.useCurrentCwd\n        |> addOption Options.dryRun\n        |> addOption Options.replace\n        |> ignore\n\n        runCommand.Action <- HistoryRun()\n\n        let deleteCommand = Command(\"delete\", Description = \"Clear history.\")\n        deleteCommand.Action <- HistoryDelete()\n\n        historyCommand.Subcommands.Add(onCommand)\n        historyCommand.Subcommands.Add(offCommand)\n        historyCommand.Subcommands.Add(showCommand)\n        historyCommand.Subcommands.Add(searchCommand)\n        historyCommand.Subcommands.Add(runCommand)\n        historyCommand.Subcommands.Add(deleteCommand)\n\n        historyCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Maintenance.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Services\nopen Grace.Shared.Parameters.Storage\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Types\nopen Spectre.Console\nopen System\nopen System.Collections.Concurrent\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Linq\nopen System.IO\nopen System.Text.Json\nopen System.Threading\nopen System.Threading.Tasks\nopen System.Collections.Generic\nopen NodaTime\n\nmodule Maintenance =\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let listDirectories =\n            new Option<bool>(\n                OptionName.ListDirectories,\n                Required = false,\n                Description = \"Show a list of directories in the Grace Index. [default: true]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> true)\n            )\n\n        let listFiles =\n            new Option<bool>(\n                OptionName.ListFiles,\n                Required = false,\n                Description = $\"Show a list of files in the Grace Index. Implies {OptionName.ListDirectories}. [default: true]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> true)\n            )\n\n        let path =\n            new Option<string>(\n                \"path\",\n                Required = false,\n                Description = \"The relative path to list. Wildcards ? and * are permitted. [default: *.*]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> \"*.*\")\n            )\n\n    let private tryGetRootSha256Hash (graceStatus: GraceStatus) =\n        let rootHashFromIndex =\n            graceStatus.Index.Values\n            |> Seq.tryFind (fun directoryVersion -> directoryVersion.RelativePath = Constants.RootDirectoryPath)\n            |> Option.map (fun directoryVersion -> directoryVersion.Sha256Hash)\n\n        let rootHashFromStatusMeta =\n            if String.IsNullOrWhiteSpace(graceStatus.RootDirectorySha256Hash) then\n                None\n            else\n                Some graceStatus.RootDirectorySha256Hash\n\n        rootHashFromIndex\n        |> Option.orElse rootHashFromStatusMeta\n\n    let private getShortHash (sha256Hash: Sha256Hash) =\n        if String.IsNullOrWhiteSpace(sha256Hash) then\n            String.Empty\n        elif sha256Hash.Length <= 8 then\n            sha256Hash\n        else\n            sha256Hash.Substring(0, 8)\n\n    let private writeRootShaSummary (graceStatus: GraceStatus) =\n        match tryGetRootSha256Hash graceStatus with\n        | Some rootSha256Hash -> AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Root SHA-256 hash: {getShortHash rootSha256Hash}[/]\")\n        | None -> AnsiConsole.MarkupLine($\"[{Colors.Error}]Root SHA-256 hash: unavailable (root directory entry missing).[/]\")\n\n    let private updateIndexHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                if parseResult |> hasOutput then\n                    let! graceStatus =\n                        progress\n                            .Columns(progressColumns)\n                            .StartAsync(fun progressContext ->\n                                task {\n                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading existing Grace index file.[/]\")\n\n                                    let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Computing new Grace index file.[/]\", autoStart = false)\n\n                                    let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Writing new Grace index file.[/]\", autoStart = false)\n\n                                    let t3 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Ensure files are in the object cache.[/]\", autoStart = false)\n\n                                    let t4 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Ensure object cache index is up-to-date.[/]\", autoStart = false)\n\n                                    let t5 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Ensure files are uploaded to object storage.[/]\", autoStart = false)\n\n                                    let t6 =\n                                        progressContext.AddTask(\n                                            $\"[{Color.DodgerBlue1}]Ensure directory versions are uploaded to Grace Server.[/]\",\n                                            autoStart = false\n                                        )\n\n                                    // Read the existing Grace status file.\n                                    t0.Increment(0.0)\n                                    let! previousGraceStatus = readGraceStatusFile ()\n                                    t0.Increment(100.0)\n\n                                    // Compute the new Grace status file, based on the contents of the working directory.\n                                    t1.StartTask()\n\n                                    let! graceStatus = createNewGraceStatusFile previousGraceStatus parseResult\n\n                                    t1.Value <- 100.0\n\n                                    // Write the new Grace status file to disk.\n                                    t2.StartTask()\n                                    do! writeGraceStatusFile graceStatus\n                                    t2.Value <- 100.0\n\n                                    // Ensure all files are in the object cache.\n                                    t3.StartTask()\n                                    let fileVersions = ConcurrentDictionary<RelativePath, LocalFileVersion>()\n\n                                    // Loop through the local directory versions, and populate fileVersions with all of the files in the repo.\n                                    let plr =\n                                        Parallel.ForEach(\n                                            graceStatus.Index.Values,\n                                            Constants.ParallelOptions,\n                                            (fun ldv ->\n                                                for fileVersion in ldv.Files do\n                                                    fileVersions.TryAdd(fileVersion.RelativePath, fileVersion)\n                                                    |> ignore)\n                                        )\n\n                                    let incrementAmount = 100.0 / double fileVersions.Count\n\n                                    // Loop through the files, and copy them to the object cache if they don't already exist.\n                                    let plr =\n                                        Parallel.ForEach(\n                                            fileVersions,\n                                            Constants.ParallelOptions,\n                                            (fun kvp _ ->\n                                                let fileVersion = kvp.Value\n                                                let fullObjectPath = fileVersion.FullObjectPath\n\n                                                if not <| File.Exists(fullObjectPath) then\n                                                    Directory.CreateDirectory(Path.GetDirectoryName(fullObjectPath))\n                                                    |> ignore // If the directory already exists, this will do nothing.\n\n                                                    File.Copy(Path.Combine(Current().RootDirectory, fileVersion.RelativePath), fullObjectPath)\n\n                                                t3.Increment(incrementAmount))\n                                        )\n\n                                    t3.Value <- 100.0\n\n                                    // Ensure the object cache index is up-to-date.\n                                    t4.StartTask()\n                                    do! upsertObjectCache graceStatus.Index.Values\n                                    t4.Value <- 100.0\n\n                                    // Ensure all files are uploaded to object storage.\n                                    t5.StartTask()\n                                    let incrementAmount = 100.0 / double fileVersions.Count\n\n                                    match Current().ObjectStorageProvider with\n                                    | ObjectStorageProvider.Unknown -> ()\n                                    | AzureBlobStorage ->\n                                        if parseResult |> verbose then\n                                            logToAnsiConsole Colors.Verbose \"Uploading files to Azure Blob Storage.\"\n\n                                        // Breaking the uploads into chunks allows us to interleave checking to see if files are already uploaded with actually uploading them when they haven't been.\n                                        let chunkSize = 32\n                                        let fileVersionGroups = fileVersions.Chunk(chunkSize)\n                                        let succeeded = ConcurrentQueue<GraceReturnValue<string>>()\n                                        let errors = ConcurrentQueue<GraceError>()\n\n                                        // Loop through the groups of file versions, and upload files that aren't already in object storage.\n                                        do!\n                                            Parallel.ForEachAsync(\n                                                fileVersionGroups,\n                                                Constants.ParallelOptions,\n                                                (fun fileVersions ct ->\n                                                    ValueTask(\n                                                        task {\n                                                            let getUploadMetadataForFilesParameters =\n                                                                GetUploadMetadataForFilesParameters(\n                                                                    OwnerId = graceIds.OwnerIdString,\n                                                                    OwnerName = graceIds.OwnerName,\n                                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                                    OrganizationName = graceIds.OrganizationName,\n                                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                                    RepositoryName = graceIds.RepositoryName,\n                                                                    CorrelationId = getCorrelationId parseResult,\n                                                                    FileVersions =\n                                                                        fileVersions\n                                                                            .Select(fun kvp -> kvp.Value.ToFileVersion)\n                                                                            .ToArray()\n                                                                )\n\n                                                            match! Storage.GetUploadMetadataForFiles getUploadMetadataForFilesParameters with\n                                                            | Ok graceReturnValue ->\n                                                                let uploadMetadata = graceReturnValue.ReturnValue\n                                                                // Increment the counter for the files that we don't have to upload.\n                                                                t5.Increment(\n                                                                    incrementAmount\n                                                                    * double (fileVersions.Count() - uploadMetadata.Count)\n                                                                )\n\n                                                                // Index all of the file versions by their SHA256 hash; we'll look up the files to upload with it.\n                                                                let filesIndexedByRelativePath =\n                                                                    Dictionary<RelativePath, LocalFileVersion>(\n                                                                        fileVersions.Select(fun kvp -> KeyValuePair(kvp.Value.RelativePath, kvp.Value))\n                                                                    )\n\n                                                                // Upload the files in this chunk to object storage.\n                                                                do!\n                                                                    Parallel.ForEachAsync(\n                                                                        uploadMetadata,\n                                                                        Constants.ParallelOptions,\n                                                                        (fun upload ct ->\n                                                                            ValueTask(\n                                                                                task {\n                                                                                    let fileVersion =\n                                                                                        filesIndexedByRelativePath[upload.RelativePath]\n                                                                                            .ToFileVersion\n\n                                                                                    let! result =\n                                                                                        Storage.SaveFileToObjectStorage\n                                                                                            (Current().RepositoryId)\n                                                                                            fileVersion\n                                                                                            (upload.BlobUriWithSasToken)\n                                                                                            (getCorrelationId parseResult)\n\n                                                                                    // Increment the counter for each file that we do upload.\n                                                                                    t5.Increment(incrementAmount)\n\n                                                                                    match result with\n                                                                                    | Ok result -> succeeded.Enqueue(result)\n                                                                                    | Error error -> errors.Enqueue(error)\n                                                                                }\n                                                                            ))\n                                                                    )\n\n                                                            | Error error -> AnsiConsole.Write((new Panel($\"{error}\")).BorderColor(Color.Red3))\n                                                        }\n                                                    ))\n                                            )\n\n                                        if errors |> Seq.isEmpty then\n                                            if parseResult |> verbose then\n                                                logToAnsiConsole Colors.Verbose \"All files uploaded successfully.\"\n\n                                            ()\n                                        else\n                                            AnsiConsole.MarkupLine($\"{errors.Count} errors occurred while uploading files to object storage.\")\n\n                                            let mutable error = GraceError.Create String.Empty String.Empty\n\n                                            while not <| errors.IsEmpty do\n                                                if errors.TryDequeue(&error) then\n                                                    AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error.EscapeMarkup()}[/]\")\n                                    | AWSS3 -> ()\n                                    | GoogleCloudStorage -> ()\n\n                                    t5.Value <- 100.0\n\n                                    // Ensure all directory versions are uploaded to Grace Server.\n                                    t6.StartTask()\n\n                                    if parseResult |> verbose then\n                                        logToAnsiConsole Colors.Verbose \"Uploading new directory versions to the server.\"\n\n                                    let chunkSize = 16\n                                    let succeeded = ConcurrentQueue<GraceReturnValue<string>>()\n                                    let errors = ConcurrentQueue<GraceError>()\n                                    let incrementAmount = 100.0 / double graceStatus.Index.Count\n\n                                    // We'll segment the uploads by the number of segments in the path,\n                                    //   so we process the deepest paths first, and the new children exist before the parent is created.\n                                    //   Within each segment group, we'll parallelize the processing for performance.\n                                    let segmentGroups =\n                                        graceStatus\n                                            .Index\n                                            .Values\n                                            .GroupBy(fun dv -> countSegments dv.RelativePath)\n                                            .OrderByDescending(fun group -> group.Key)\n\n                                    for group in segmentGroups do\n                                        let directoryVersionGroups = group.Chunk(chunkSize)\n                                        let parallelOptions = System.Threading.Tasks.ParallelOptions(MaxDegreeOfParallelism = 3)\n\n                                        do!\n                                            Parallel.ForEachAsync(\n                                                directoryVersionGroups,\n                                                parallelOptions,\n                                                (fun (directoryVersionGroup: LocalDirectoryVersion array) (ct: CancellationToken) ->\n                                                    ValueTask(\n                                                        task {\n                                                            let saveDirectoryVersionsParameters = SaveDirectoryVersionsParameters()\n                                                            saveDirectoryVersionsParameters.OwnerId <- graceIds.OwnerIdString\n                                                            saveDirectoryVersionsParameters.OwnerName <- graceIds.OwnerName\n                                                            saveDirectoryVersionsParameters.OrganizationId <- graceIds.OrganizationIdString\n                                                            saveDirectoryVersionsParameters.OrganizationName <- graceIds.OrganizationName\n                                                            saveDirectoryVersionsParameters.RepositoryId <- graceIds.RepositoryIdString\n                                                            saveDirectoryVersionsParameters.RepositoryName <- graceIds.RepositoryName\n                                                            saveDirectoryVersionsParameters.CorrelationId <- getCorrelationId parseResult\n\n                                                            saveDirectoryVersionsParameters.DirectoryVersions <-\n                                                                directoryVersionGroup\n                                                                    .Select(fun dv -> dv.ToDirectoryVersion)\n                                                                    .ToList()\n\n                                                            match! DirectoryVersion.SaveDirectoryVersions saveDirectoryVersionsParameters with\n                                                            | Ok result -> succeeded.Enqueue(result)\n                                                            | Error error -> errors.Enqueue(error)\n\n                                                            t6.Increment(\n                                                                incrementAmount\n                                                                * double directoryVersionGroup.Length\n                                                            )\n                                                        }\n                                                    ))\n                                            )\n\n                                    t6.Value <- 100.0\n\n                                    AnsiConsole.MarkupLine($\"[{Colors.Important}]succeeded: {succeeded.Count}; errors: {errors.Count}.[/]\")\n\n                                    let mutable error = GraceError.Create String.Empty String.Empty\n\n                                    while not <| errors.IsEmpty do\n                                        errors.TryDequeue(&error) |> ignore\n\n                                        if error.Error.Contains(\"TRetval\") then logToConsole $\"********* {error.Error}\"\n\n                                        AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error.EscapeMarkup()}[/]\")\n\n                                    return graceStatus\n                                })\n\n                    let fileCount =\n                        graceStatus\n                            .Index\n                            .Values\n                            .Select(fun directoryVersion -> directoryVersion.Files.Count)\n                            .Sum()\n\n                    let totalFileSize = graceStatus.Index.Values.Sum(fun directoryVersion -> directoryVersion.Files.Sum(fun f -> int64 f.Size))\n\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of directories scanned: {graceStatus.Index.Count}.[/]\")\n\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of files scanned: {fileCount}; total file size: {totalFileSize:N0}.[/]\")\n\n                    writeRootShaSummary graceStatus\n                    return 0\n                else\n                    let! previousGraceStatus = readGraceStatusFile ()\n                    let! graceStatus = createNewGraceStatusFile previousGraceStatus parseResult\n                    do! writeGraceStatusFile graceStatus\n\n                    let fileVersions = ConcurrentDictionary<RelativePath, LocalFileVersion>()\n\n                    let plr =\n                        Parallel.ForEach(\n                            graceStatus.Index.Values,\n                            Constants.ParallelOptions,\n                            (fun ldv ->\n                                for fileVersion in ldv.Files do\n                                    fileVersions.TryAdd(fileVersion.RelativePath, fileVersion)\n                                    |> ignore)\n                        )\n\n                    let plr =\n                        Parallel.ForEach(\n                            fileVersions,\n                            Constants.ParallelOptions,\n                            (fun kvp _ ->\n                                let fileVersion = kvp.Value\n                                let fullObjectPath = fileVersion.FullObjectPath\n\n                                if not <| File.Exists(fullObjectPath) then\n                                    Directory.CreateDirectory(Path.GetDirectoryName(fullObjectPath))\n                                    |> ignore\n\n                                    File.Copy(Path.Combine(Current().RootDirectory, fileVersion.RelativePath), fullObjectPath))\n                        )\n\n                    match Current().ObjectStorageProvider with\n                    | ObjectStorageProvider.Unknown -> return -1\n                    | AzureBlobStorage ->\n                        let chunkSize = 32\n                        let fileVersionGroups = fileVersions.Chunk(chunkSize)\n                        let succeeded = ConcurrentQueue<GraceReturnValue<string>>()\n                        let errors = ConcurrentQueue<GraceError>()\n\n                        do!\n                            Parallel.ForEachAsync(\n                                fileVersionGroups,\n                                Constants.ParallelOptions,\n                                (fun fileVersions ct ->\n                                    ValueTask(\n                                        task {\n                                            let getUploadMetadataForFilesParameters =\n                                                GetUploadMetadataForFilesParameters(\n                                                    OwnerId = graceIds.OwnerIdString,\n                                                    OwnerName = graceIds.OwnerName,\n                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                    OrganizationName = graceIds.OrganizationName,\n                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                    RepositoryName = graceIds.RepositoryName,\n                                                    CorrelationId = getCorrelationId parseResult,\n                                                    FileVersions =\n                                                        (fileVersions\n                                                         |> Seq.map (fun kvp -> kvp.Value.ToFileVersion)\n                                                         |> Seq.toArray)\n                                                )\n\n                                            match! Storage.GetUploadMetadataForFiles getUploadMetadataForFilesParameters with\n                                            | Ok graceReturnValue ->\n                                                let uploadMetadata = graceReturnValue.ReturnValue\n\n                                                let filesIndexedBySha256Hash =\n                                                    Dictionary<Sha256Hash, LocalFileVersion>(\n                                                        fileVersions.Select(fun kvp -> KeyValuePair(kvp.Value.Sha256Hash, kvp.Value))\n                                                    )\n\n                                                do!\n                                                    Parallel.ForEachAsync(\n                                                        uploadMetadata,\n                                                        Constants.ParallelOptions,\n                                                        (fun upload ct ->\n                                                            ValueTask(\n                                                                task {\n                                                                    let fileVersion =\n                                                                        filesIndexedBySha256Hash[upload.Sha256Hash]\n                                                                            .ToFileVersion\n\n                                                                    let! result =\n                                                                        Storage.SaveFileToObjectStorage\n                                                                            (Current().RepositoryId)\n                                                                            fileVersion\n                                                                            (upload.BlobUriWithSasToken)\n                                                                            (getCorrelationId parseResult)\n\n                                                                    match result with\n                                                                    | Ok result -> succeeded.Enqueue(result)\n                                                                    | Error error -> errors.Enqueue(error)\n                                                                }\n                                                            ))\n                                                    )\n                                            | Error error -> AnsiConsole.MarkupLine($\"[{Colors.Error}]{error}[/]\")\n                                        }\n                                    ))\n                            )\n\n                        if errors |> Seq.isEmpty then\n                            return 0\n                        else\n                            AnsiConsole.MarkupLine($\"{errors.Count} errors occurred.\")\n                            let mutable error = GraceError.Create String.Empty String.Empty\n\n                            while not <| errors.IsEmpty do\n                                if errors.TryDequeue(&error) then\n                                    AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error.EscapeMarkup()}[/]\")\n\n                            return -1\n\n                    | AWSS3 -> return 0\n                    | GoogleCloudStorage -> return 0\n\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"Exception in UpdateIndex: {ExceptionResponse.Create ex}\"\n                return -1\n        }\n\n    type UpdateIndex() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task { return! updateIndexHandler parseResult }\n\n    type Scan() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n\n                if parseResult |> hasOutput then\n                    let! (differences, newDirectoryVersions) =\n                        progress\n                            .Columns(progressColumns)\n                            .StartAsync(fun progressContext ->\n                                task {\n                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace index file.[/]\")\n\n                                    let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Scanning working directory for changes.[/]\", autoStart = false)\n\n                                    let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Computing root directory SHA-256 value.[/]\", autoStart = false)\n\n                                    t0.Increment(0.0)\n                                    let! previousGraceStatus = readGraceStatusFile ()\n                                    t0.Increment(100.0)\n                                    t1.StartTask()\n                                    t1.Increment(0.0)\n                                    let! differences = scanForDifferences previousGraceStatus\n                                    t1.Increment(100.0)\n                                    t2.StartTask()\n                                    t2.Increment(0.0)\n\n                                    let! (newGraceIndex, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                                    t2.Increment(100.0)\n                                    return (differences, newDirectoryVersions)\n                                })\n\n                    AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Number of differences: {differences.Count}[/]\"\n\n                    for difference in differences do\n                        let x = sprintf \"%A\" difference\n                        AnsiConsole.MarkupLine $\"[{Colors.Important}]{x}[/]\"\n\n                    AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Number of new DirectoryVersions: {newDirectoryVersions.Count}[/]\"\n\n                    for ldv in newDirectoryVersions do\n                        AnsiConsole.MarkupLine\n                            $\"[{Colors.Important}]SHA-256: {ldv.Sha256Hash.Substring(0, 8)}; DirectoryId: {ldv.DirectoryVersionId}; RelativePath: {ldv.RelativePath}[/]\"\n                //AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Root SHA-256 hash: {rootDirectoryVersion.Sha256Hash.Substring(8)}[/]\"\n                else\n                    let! previousGraceStatus = readGraceStatusFile ()\n                    let! differences = scanForDifferences previousGraceStatus\n\n                    let! (newGraceIndex, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                    AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Number of differences: {differences.Count}[/]\"\n\n                    for difference in differences do\n                        let x = sprintf \"%A\" difference\n                        AnsiConsole.MarkupLine $\"[{Colors.Important}]{x}[/]\"\n\n                    AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]newDirectoryVersions.Count: {newDirectoryVersions.Count}[/]\"\n\n                    for ldv in newDirectoryVersions do\n                        AnsiConsole.MarkupLine\n                            $\"[{Colors.Important}]SHA-256: {ldv.Sha256Hash.Substring(0, 8)}; DirectoryId: {ldv.DirectoryVersionId}; RelativePath: {ldv.RelativePath}[/]\"\n\n                return 0\n            }\n\n    type Stats() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n\n                let! graceStatus = readGraceStatusFile ()\n                let directoryCount = graceStatus.Index.Count\n\n                let fileCount =\n                    graceStatus\n                        .Index\n                        .Values\n                        .Select(fun directoryVersion -> directoryVersion.Files.Count)\n                        .Sum()\n\n                let totalFileSize = graceStatus.Index.Values.Sum(fun directoryVersion -> directoryVersion.Files.Sum(fun f -> int64 f.Size))\n\n                AnsiConsole.MarkupLine($\"[{Colors.Important}]All values taken from the local Grace status file.[/]\")\n                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of directories: {directoryCount}.[/]\")\n\n                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of files: {fileCount}; total file size: {totalFileSize:N0}.[/]\")\n\n                writeRootShaSummary graceStatus\n\n                return 0\n            }\n\n    type ListContents() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n\n                let listDirectories = parseResult.GetValue(Options.listDirectories)\n                let listFiles = parseResult.GetValue(Options.listFiles)\n\n                let! graceStatus = readGraceStatusFile ()\n                let directoryCount = graceStatus.Index.Count\n\n                let fileCount =\n                    graceStatus\n                        .Index\n                        .Values\n                        .Select(fun directoryVersion -> directoryVersion.Files.Count)\n                        .Sum()\n\n                let totalFileSize = graceStatus.Index.Values.Sum(fun directoryVersion -> directoryVersion.Files.Sum(fun f -> int64 f.Size))\n\n                AnsiConsole.MarkupLine($\"[{Colors.Important}]All values taken from the local Grace status file.[/]\")\n                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of directories: {directoryCount}.[/]\")\n\n                AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of files: {fileCount}; total file size: {totalFileSize:N0}.[/]\")\n\n                writeRootShaSummary graceStatus\n\n                if listDirectories then\n                    if directoryCount = 0 then\n                        AnsiConsole.MarkupLine($\"[{Colors.Verbose}]No directory entries found in the local Grace status file.[/]\")\n                    else\n                        let longestRelativePath = getLongestRelativePath graceStatus.Index.Values\n                        let additionalSpaces = String.replicate (longestRelativePath - 2) \" \"\n                        let additionalImportantDashes = String.replicate (longestRelativePath + 3) \"-\"\n                        let additionalDeemphasizedDashes = String.replicate 38 \"-\"\n\n                        let sortedDirectoryVersions = graceStatus.Index.Values.OrderBy(fun dv -> dv.RelativePath)\n\n                        sortedDirectoryVersions\n                        |> Seq.iteri (fun i directoryVersion ->\n                            AnsiConsole.WriteLine()\n\n                            if i = 0 then\n                                AnsiConsole.MarkupLine(\n                                    $\"[{Colors.Important}]Last Write Time (UTC)        SHA-256            Size  Path{additionalSpaces}[/][{Colors.Deemphasized}] (DirectoryVersionId)[/]\"\n                                )\n\n                                AnsiConsole.MarkupLine(\n                                    $\"[{Colors.Important}]-----------------------------------------------------{additionalImportantDashes}[/][{Colors.Deemphasized}] {additionalDeemphasizedDashes}[/]\"\n                                )\n\n                            let rightAlignedDirectoryVersionId =\n                                (String.replicate\n                                    (longestRelativePath\n                                     - directoryVersion.RelativePath.Length)\n                                    \" \")\n                                + $\"({directoryVersion.DirectoryVersionId})\"\n\n                            AnsiConsole.MarkupLine(\n                                $\"[{Colors.Highlighted}]{formatDateTimeAligned directoryVersion.LastWriteTimeUtc}   {getShortSha256Hash directoryVersion.Sha256Hash}  {directoryVersion.Size, 13:N0}  /{directoryVersion.RelativePath}[/] [{Colors.Deemphasized}] {rightAlignedDirectoryVersionId}[/]\"\n                            )\n\n                            if listFiles then\n                                let sortedFiles = directoryVersion.Files.OrderBy(fun f -> f.RelativePath)\n\n                                for file in sortedFiles do\n                                    AnsiConsole.MarkupLine(\n                                        $\"[{Colors.Verbose}]{formatDateTimeAligned file.LastWriteTimeUtc}   {getShortSha256Hash file.Sha256Hash}  {file.Size, 13:N0}  |- {file.FileInfo.Name}[/]\"\n                                    ))\n\n                return 0\n            }\n\n    type CheckIgnoreEntries() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                if parseResult |> verbose then printParseResult parseResult\n                AnsiConsole.MarkupLine($\"[{Colors.Important}]Directory ignore entries:[/]\")\n\n                for dir in Current().GraceDirectoryIgnoreEntries do\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]  {dir}[/]\")\n\n                AnsiConsole.WriteLine()\n                AnsiConsole.MarkupLine($\"[{Colors.Important}]File ignore entries:[/]\")\n\n                for file in Current().GraceFileIgnoreEntries do\n                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]  {file}[/]\")\n\n                return 0\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryId\n            |> addOption Options.repositoryName\n\n        let maintenanceCommand = new Command(\"maintenance\", Description = \"Performs various maintenance tasks.\")\n\n        maintenanceCommand.Aliases.Add(\"maint\")\n\n        let updateIndexCommand =\n            new Command(\"update-index\", Description = \"Recreates the local Grace index file based on the current working directory contents.\")\n            |> addCommonOptions\n\n        updateIndexCommand.Action <- new UpdateIndex()\n        maintenanceCommand.Subcommands.Add(updateIndexCommand)\n\n        let scanCommand =\n            new Command(\"scan\", Description = \"Scans the working directory contents for changes.\")\n            |> addCommonOptions\n\n        scanCommand.Action <- new Scan()\n        maintenanceCommand.Subcommands.Add(scanCommand)\n\n        let statsCommand =\n            new Command(\"stats\", Description = \"Displays statistics about the current working directory.\")\n            |> addCommonOptions\n\n        statsCommand.Action <- new Stats()\n        maintenanceCommand.Subcommands.Add(statsCommand)\n\n        let listContentsCommand =\n            new Command(\"list-contents\", Description = \"List directories and files from the Grace Status file.\")\n            |> addCommonOptions\n            |> addOption Options.listDirectories\n            |> addOption Options.listFiles\n\n        listContentsCommand.Action <- new ListContents()\n        maintenanceCommand.Subcommands.Add(listContentsCommand)\n\n        let checkIgnoreEntriesCommand =\n            new Command(\"check-ignore-entries\", Description = \"Check the .graceignore entries for validity.\")\n            |> addCommonOptions\n\n        checkIgnoreEntriesCommand.Action <- new CheckIgnoreEntries()\n        maintenanceCommand.Subcommands.Add(checkIgnoreEntriesCommand)\n\n        maintenanceCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Organization.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen NodaTime\nopen Spectre.Console\nopen Spectre.Console.Json\nopen System\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.Linq\nopen System.Threading\nopen System.Threading.Tasks\nopen Grace.CLI\n\nmodule Organization =\n\n    type CommonParameters() =\n        inherit ParameterBase()\n        member val public OwnerId: string = String.Empty with get, set\n        member val public OwnerName: string = String.Empty with get, set\n        member val public OrganizationId: string = String.Empty with get, set\n        member val public OrganizationName: string = String.Empty with get, set\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The organization's owner ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The organization's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The name of the organization. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationNameRequired =\n            new Option<String>(OptionName.OrganizationName, Required = true, Description = \"The name of the organization.\", Arity = ArgumentArity.ExactlyOne)\n\n        let organizationType =\n            (new Option<String>(\n                OptionName.OrganizationType,\n                Required = true,\n                Description = \"The type of the organization. [default: Public]\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<OrganizationType> ())\n\n        let searchVisibility =\n            (new Option<String>(\n                OptionName.SearchVisibility,\n                Required = true,\n                Description = \"Enables or disables the organization appearing in searches. [default: Visible]\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<SearchVisibility> ())\n\n        let description =\n            new Option<String>(OptionName.Description, Required = true, Description = \"Description of the organization.\", Arity = ArgumentArity.ExactlyOne)\n\n        let newName =\n            new Option<String>(OptionName.NewName, Required = true, Description = \"The new name of the organization.\", Arity = ArgumentArity.ExactlyOne)\n\n        let force = new Option<bool>(OptionName.Force, Required = false, Description = \"Delete even if there is data under this organization. [default: false]\")\n\n        let includeDeleted =\n            new Option<bool>(\n                OptionName.IncludeDeleted,\n                [| \"-d\" |],\n                Required = false,\n                Description = \"Include deleted organizations in the result. [default: false]\"\n            )\n\n        let deleteReason =\n            new Option<String>(\n                OptionName.DeleteReason,\n                Required = true,\n                Description = \"The reason for deleting the organization.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let doNotSwitch =\n            new Option<bool>(\n                OptionName.DoNotSwitch,\n                Required = false,\n                Description =\n                    \"Do not switch your current organization to the new organization after it is created. By default, the new organization becomes the current organization.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n    // Create subcommand.\n    type Create() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    // In a Create() command, if --organization-id is implicit, that's actually the old OrganizationId taken from graceconfig.json,\n                    //   and we need to set OrganizationId to a new Guid.\n                    let mutable graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    if parseResult\n                        .GetResult(\n                            Options.organizationId\n                        )\n                        .Implicit then\n                        let organizationId = Guid.NewGuid()\n                        graceIds <- { graceIds with OrganizationId = organizationId; OrganizationIdString = $\"{organizationId}\" }\n\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let organizationId =\n                            if parseResult\n                                .GetResult(\n                                    Options.organizationId\n                                )\n                                .Implicit then\n                                Guid.NewGuid().ToString()\n                            else\n                                graceIds.OrganizationIdString\n\n                        let parameters =\n                            Parameters.Organization.CreateOrganizationParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = organizationId,\n                                OrganizationName = graceIds.OrganizationName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.Create(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            match result with\n                            | Ok returnValue ->\n                                if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                    let newConfig = Current()\n                                    newConfig.OrganizationId <- Guid.Parse($\"{returnValue.Properties[nameof OrganizationId]}\")\n                                    newConfig.OrganizationName <- $\"{returnValue.Properties[nameof OrganizationName]}\"\n                                    updateConfiguration newConfig\n\n                                return result |> renderOutput parseResult\n                            | Error _ -> return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.Create(parameters)\n\n                            match result with\n                            | Ok returnValue ->\n                                if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                    let newConfig = Current()\n                                    newConfig.OrganizationId <- Guid.Parse($\"{returnValue.Properties[nameof OrganizationId]}\")\n                                    newConfig.OrganizationName <- $\"{returnValue.Properties[nameof OrganizationName]}\"\n                                    updateConfiguration newConfig\n\n                                return result |> renderOutput parseResult\n                            | Error _ -> return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Get subcommand\n    type Get() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.GetOrganizationParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                IncludeDeleted = parseResult.GetValue(Options.includeDeleted),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.Get(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            match result with\n                            | Ok graceReturnValue ->\n                                let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                                AnsiConsole.Write(jsonText)\n                                AnsiConsole.WriteLine()\n                                return Ok graceReturnValue |> renderOutput parseResult\n                            | Error graceError -> return Error graceError |> renderOutput parseResult\n                        else\n                            let! result = Organization.Get(parameters)\n\n                            match result with\n                            | Ok graceReturnValue ->\n                                let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                                AnsiConsole.Write(jsonText)\n                                AnsiConsole.WriteLine()\n                                return Ok graceReturnValue |> renderOutput parseResult\n                            | Error graceError -> return Error graceError |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetName subcommand\n    type SetName() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.SetOrganizationNameParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                NewName = parseResult.GetValue(Options.newName),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.SetName(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.SetName(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Organization.SetType subcommand definition\n    type SetType() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.SetOrganizationTypeParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                OrganizationType = parseResult.GetValue(Options.organizationType),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.SetType(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.SetType(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetSearchVisibility subcommand\n    type SetSearchVisibility() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.SetOrganizationSearchVisibilityParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                SearchVisibility = parseResult.GetValue(Options.searchVisibility),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.SetSearchVisibility(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.SetSearchVisibility(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetDescription subcommand\n    type SetDescription() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.SetOrganizationDescriptionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                Description = parseResult.GetValue(Options.description),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.SetDescription(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.SetDescription(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Delete subcommand\n    type Delete() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.DeleteOrganizationParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                Force = parseResult.GetValue(Options.force),\n                                DeleteReason = parseResult.GetValue(Options.deleteReason),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.Delete(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.Delete(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Undelete subcommand\n    type Undelete() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Organization.DeleteOrganizationParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Organization.Delete(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Organization.Delete(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    let Build =\n        let addCommonOptionsWithoutOrganizationName (command: Command) =\n            command\n            |> addOption Options.organizationId\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.organizationName\n            |> addCommonOptionsWithoutOrganizationName\n\n        // Create main command and aliases, if any.\n        let organizationCommand = new Command(\"organization\", Description = \"Create, change, or delete organization-level information.\")\n\n        organizationCommand.Aliases.Add(\"org\")\n\n        // Add subcommands.\n        let organizationCreateCommand =\n            new Command(\"create\", Description = \"Create a new organization.\")\n            |> addOption Options.organizationNameRequired\n            |> addCommonOptionsWithoutOrganizationName\n            |> addOption Options.doNotSwitch\n\n        organizationCreateCommand.Action <- new Create()\n        organizationCommand.Subcommands.Add(organizationCreateCommand)\n\n        let getCommand =\n            new Command(\"get\", Description = \"Gets details for the organization.\")\n            |> addOption Options.includeDeleted\n            |> addCommonOptions\n\n        getCommand.Action <- new Get()\n        organizationCommand.Subcommands.Add(getCommand)\n\n        let setNameCommand =\n            new Command(\"set-name\", Description = \"Change the name of the organization.\")\n            |> addOption Options.newName\n            |> addCommonOptions\n\n        setNameCommand.Action <- new SetName()\n        organizationCommand.Subcommands.Add(setNameCommand)\n\n        let setTypeCommand =\n            new Command(\"set-type\", Description = \"Change the type of the organization.\")\n            |> addOption Options.organizationType\n            |> addCommonOptions\n\n        setTypeCommand.Action <- new SetType()\n        organizationCommand.Subcommands.Add(setTypeCommand)\n\n        let setSearchVisibilityCommand =\n            new Command(\"set-search-visibility\", Description = \"Change the search visibility of the organization.\")\n            |> addOption Options.searchVisibility\n            |> addCommonOptions\n\n        setSearchVisibilityCommand.Action <- new SetSearchVisibility()\n        organizationCommand.Subcommands.Add(setSearchVisibilityCommand)\n\n        let setDescriptionCommand =\n            new Command(\"set-description\", Description = \"Change the description of the organization.\")\n            |> addOption Options.description\n            |> addCommonOptions\n\n        setDescriptionCommand.Action <- new SetDescription()\n        organizationCommand.Subcommands.Add(setDescriptionCommand)\n\n        let deleteCommand =\n            new Command(\"delete\", Description = \"Delete the organization.\")\n            |> addOption Options.force\n            |> addOption Options.deleteReason\n            |> addCommonOptions\n\n        deleteCommand.Action <- new Delete()\n        organizationCommand.Subcommands.Add(deleteCommand)\n\n        let undeleteCommand =\n            new Command(\"undelete\", Description = \"Undeletes the organization.\")\n            |> addCommonOptions\n\n        undeleteCommand.Action <- new Undelete()\n        organizationCommand.Subcommands.Add(undeleteCommand)\n\n        organizationCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Owner.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Client.Theme\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Types\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Linq\nopen System.Threading\nopen System.Threading.Tasks\nopen System.CommandLine\nopen Spectre.Console\nopen Spectre.Console.Json\nopen Grace.Shared.Utilities\nopen Grace.CLI\nopen Grace.CLI.Services\n\nmodule Owner =\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                [||],\n                Required = false,\n                Description = \"The Id of the owner <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The name of the owner. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerNameRequired =\n            new Option<String>(OptionName.OwnerName, Required = true, Description = \"The name of the owner.\", Arity = ArgumentArity.ExactlyOne)\n\n        let ownerTypeRequired =\n            (new Option<String>(OptionName.OwnerType, Required = true, Description = \"The type of owner. [default: Public]\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(Utilities.listCases<OwnerType> ())\n\n        let searchVisibilityRequired =\n            (new Option<String>(\n                OptionName.SearchVisibility,\n                Required = true,\n                Description = \"Enables or disables the owner appearing in searches. [default: true]\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(Utilities.listCases<SearchVisibility> ())\n\n        let descriptionRequired =\n            new Option<String>(OptionName.Description, Required = true, Description = \"Description of the owner.\", Arity = ArgumentArity.ExactlyOne)\n\n        let newName =\n            new Option<String>(OptionName.NewName, Required = true, Description = \"The new name of the organization.\", Arity = ArgumentArity.ExactlyOne)\n\n        let force = new Option<bool>(OptionName.Force, Required = false, Description = \"Delete even if there is data under this owner. [default: false]\")\n\n        let includeDeleted =\n            new Option<bool>(OptionName.IncludeDeleted, [| \"-d\" |], Required = false, Description = \"Include deleted owners in the result. [default: false]\")\n\n        let deleteReason =\n            new Option<String>(OptionName.DeleteReason, Required = true, Description = \"The reason for deleting the owner.\", Arity = ArgumentArity.ExactlyOne)\n\n        let doNotSwitch =\n            new Option<bool>(\n                OptionName.DoNotSwitch,\n                Required = false,\n                Description = \"Do not switch your current owner to the new owner after it is created. By default, the new owner becomes the current owner.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n    let ownerCommonValidations =\n        CommonValidations\n        >=> ``Either OwnerId or OwnerName must be provided``\n\n    // Create subcommand.\n    type Create() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    // In a Create() command, if --owner-id is implicit, that's actually the old OwnerId taken from graceconfig.json,\n                    //   and we need to set OwnerId to a new Guid.\n                    let mutable graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    if parseResult.GetResult(Options.ownerId).Implicit then\n                        let ownerId = Guid.NewGuid()\n                        graceIds <- { graceIds with OwnerId = ownerId; OwnerIdString = $\"{ownerId}\" }\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> ownerCommonValidations\n                        >>= (``Option must be present`` OptionName.OwnerName OwnerNameIsRequired)\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let ownerId =\n                            if parseResult.GetResult(Options.ownerId).Implicit then\n                                Guid.NewGuid().ToString()\n                            else\n                                graceIds.OwnerIdString\n\n                        let parameters =\n                            Parameters.Owner.CreateOwnerParameters(OwnerId = ownerId, OwnerName = graceIds.OwnerName, CorrelationId = graceIds.CorrelationId)\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.Create(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            match result with\n                            | Ok returnValue ->\n                                // Update the Grace configuration file with the newly-created owner.\n                                if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                    let newConfig = Current()\n                                    newConfig.OwnerId <- Guid.Parse($\"{returnValue.Properties[nameof OwnerId]}\")\n                                    newConfig.OwnerName <- $\"{returnValue.Properties[nameof OwnerName]}\"\n                                    logToAnsiConsole Colors.Verbose $\"newConfig: {serialize newConfig}.\"\n                                    updateConfiguration newConfig\n\n                                return result |> renderOutput parseResult\n                            | Error _ -> return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.Create(parameters)\n\n                            match result with\n                            | Ok returnValue ->\n                                // Update the Grace configuration file with the newly-created owner.\n                                if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                    let newConfig = Current()\n                                    newConfig.OwnerId <- Guid.Parse($\"{returnValue.Properties[nameof OwnerId]}\")\n                                    newConfig.OwnerName <- $\"{returnValue.Properties[nameof OwnerName]}\"\n                                    logToAnsiConsole Colors.Verbose $\"newConfig: {serialize newConfig}.\"\n                                    updateConfiguration newConfig\n\n                                return result |> renderOutput parseResult\n                            | Error _ -> return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Get subcommand\n    type Get() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.GetOwnerParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                IncludeDeleted = parseResult.GetValue(Options.includeDeleted),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.Get(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            match result with\n                            | Ok graceReturnValue ->\n                                let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                                AnsiConsole.Write(jsonText)\n                                AnsiConsole.WriteLine()\n                                return Ok graceReturnValue |> renderOutput parseResult\n                            | Error graceError -> return Error graceError |> renderOutput parseResult\n                        else\n                            let! result = Owner.Get(parameters)\n\n                            match result with\n                            | Ok graceReturnValue ->\n                                let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                                AnsiConsole.Write(jsonText)\n                                AnsiConsole.WriteLine()\n                                return Ok graceReturnValue |> renderOutput parseResult\n                            | Error graceError -> return Error graceError |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetName subcommand\n    type SetName() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.SetOwnerNameParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                NewName = parseResult.GetValue(Options.newName),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.SetName(parameters)\n\n                                            match result with\n                                            | Ok returnValue ->\n                                                // Update the Grace configuration file with the new Owner name.\n                                                let newConfig = Current()\n                                                newConfig.OwnerName <- parseResult.GetValue(Options.newName)\n                                                updateConfiguration newConfig\n                                            | Error _ -> ()\n\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.SetName(parameters)\n\n                            match result with\n                            | Ok graceReturnValue ->\n                                // Update the Grace configuration file with the new Owner name.\n                                let newConfig = Current()\n                                newConfig.OwnerName <- parseResult.GetValue(Options.newName)\n                                updateConfiguration newConfig\n                            | Error _ -> ()\n\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetType subcommand\n    type SetType() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.SetOwnerTypeParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OwnerType = parseResult.GetValue(Options.ownerTypeRequired),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.SetType(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.SetType(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetSearchVisibility subcommand\n    type SetSearchVisibility() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.SetOwnerSearchVisibilityParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                SearchVisibility = parseResult.GetValue(Options.searchVisibilityRequired),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.SetSearchVisibility(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.SetSearchVisibility(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // SetDescription subcommand\n    type SetDescription() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.SetOwnerDescriptionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                Description = parseResult.GetValue(Options.descriptionRequired),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.SetDescription(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.SetDescription(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Delete subcommand\n    type Delete() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.DeleteOwnerParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                Force = parseResult.GetValue(Options.force),\n                                DeleteReason = parseResult.GetValue(Options.deleteReason),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.Delete(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.Delete(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Undelete subcommand\n    type Undelete() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ownerCommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Owner.UndeleteOwnerParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Owner.Undelete(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            return result |> renderOutput parseResult\n                        else\n                            let! result = Owner.Undelete(parameters)\n                            return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n\n        // Create main command and aliases, if any.`\n        let ownerCommand = new Command(\"owner\", Description = \"Create, change, or delete owner-level information.\")\n\n        // Add subcommands.\n        let ownerCreateCommand =\n            new Command(\"create\", Description = \"Create a new owner.\")\n            |> addOption Options.ownerNameRequired\n            |> addOption Options.ownerId\n            |> addOption Options.doNotSwitch\n\n        ownerCreateCommand.Action <- new Create()\n        ownerCommand.Subcommands.Add(ownerCreateCommand)\n\n        let getCommand =\n            new Command(\"get\", Description = \"Gets details for the owner.\")\n            |> addOption Options.includeDeleted\n            |> addCommonOptions\n\n        getCommand.Action <- new Get()\n        ownerCommand.Subcommands.Add(getCommand)\n\n        let setNameCommand =\n            new Command(\"set-name\", Description = \"Change the name of the owner.\")\n            |> addOption Options.newName\n            |> addCommonOptions\n\n        setNameCommand.Action <- new SetName()\n        ownerCommand.Subcommands.Add(setNameCommand)\n\n        let setTypeCommand =\n            new Command(\"set-type\", Description = \"Change the type of the owner.\")\n            |> addOption Options.ownerTypeRequired\n            |> addCommonOptions\n\n        setTypeCommand.Action <- new SetType()\n        ownerCommand.Subcommands.Add(setTypeCommand)\n\n        let setSearchVisibilityCommand =\n            new Command(\"set-search-visibility\", Description = \"Change the search visibility of the owner.\")\n            |> addOption Options.searchVisibilityRequired\n            |> addCommonOptions\n\n        setSearchVisibilityCommand.Action <- new SetSearchVisibility()\n        ownerCommand.Subcommands.Add(setSearchVisibilityCommand)\n\n        let setDescriptionCommand =\n            new Command(\"set-description\", Description = \"Change the description of the owner.\")\n            |> addOption Options.descriptionRequired\n            |> addCommonOptions\n\n        setDescriptionCommand.Action <- new SetDescription()\n        ownerCommand.Subcommands.Add(setDescriptionCommand)\n\n        let deleteCommand =\n            new Command(\"delete\", Description = \"Delete the owner.\")\n            |> addOption Options.force\n            |> addOption Options.deleteReason\n            |> addCommonOptions\n\n        deleteCommand.Action <- new Delete()\n        ownerCommand.Subcommands.Add(deleteCommand)\n\n        let undeleteCommand =\n            new Command(\"undelete\", Description = \"Undelete a deleted owner.\")\n            |> addCommonOptions\n\n        undeleteCommand.Action <- new Undelete()\n        ownerCommand.Subcommands.Add(undeleteCommand)\n\n        ownerCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/PromotionSet.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.PromotionSet\nopen Grace.Types.Types\nopen Spectre.Console\nopen System\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.IO\nopen System.Text.Json\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule PromotionSetCommand =\n    module private Options =\n        let promotionSetId =\n            new Option<string>(\n                \"--promotion-set\",\n                [|\n                    \"--promotion-set-id\"\n                    \"--promotionSetId\"\n                |],\n                Required = true,\n                Description = \"The promotion set ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let promotionSetIdOptional =\n            new Option<string>(\n                \"--promotion-set\",\n                [|\n                    \"--promotion-set-id\"\n                    \"--promotionSetId\"\n                |],\n                Required = false,\n                Description = \"The promotion set ID <Guid>. If omitted, a new one is generated.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let targetBranchId =\n            new Option<string>(\n                \"--target-branch-id\",\n                [| \"--target-branch\" |],\n                Required = true,\n                Description = \"The target branch ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let promotionPointersFile =\n            new Option<string>(\n                \"--promotion-pointers-file\",\n                [| \"--pointers-file\"; \"--input-file\" |],\n                Required = true,\n                Description = \"Path to a JSON file containing PromotionPointer entries.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let reason =\n            new Option<string>(\n                \"--reason\",\n                [| \"--recompute-reason\" |],\n                Required = false,\n                Description = \"Optional reason for recomputing the promotion set.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let force =\n            new Option<bool>(\n                OptionName.Force,\n                [| \"-f\"; \"--force\" |],\n                Required = false,\n                Description = \"Force logical deletion of the promotion set.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let deleteReason =\n            new Option<string>(\n                OptionName.DeleteReason,\n                Required = false,\n                Description = \"Optional reason for deleting the promotion set.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let stepId =\n            new Option<string>(\n                \"--step\",\n                [| \"--step-id\"; \"--stepId\" |],\n                Required = true,\n                Description = \"The promotion set step ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let decisionsFile =\n            new Option<string>(\n                \"--decisions-file\",\n                [| \"--decisionsFile\" |],\n                Required = true,\n                Description = \"Path to a JSON file containing ConflictResolutionDecision entries.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The organization's name. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                Required = false,\n                Description = \"The repository's name. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    type private ConflictStepDisplay =\n        {\n            StepId: PromotionSetStepId\n            Order: int\n            ConflictStatus: string\n            ConflictSummaryArtifactId: ArtifactId option\n            DownloadUri: UriWithSharedAccessSignature option\n            DownloadUriError: string option\n        }\n\n    type private ConflictShowResult =\n        {\n            PromotionSetId: PromotionSetId\n            Status: string\n            StepsComputationStatus: string\n            StepsComputationAttempt: int\n            ConflictedSteps: ConflictStepDisplay list\n        }\n\n    type private ConflictDecisionsWrapper = { Decisions: ConflictResolutionDecision list }\n    type private PromotionPointersWrapper = { PromotionPointers: PromotionPointer list }\n\n    let private tryParseGuid (value: string) (errorMessage: string) (parseResult: ParseResult) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value)\n           || Guid.TryParse(value, &parsed) = false\n           || parsed = Guid.Empty then\n            Error(GraceError.Create errorMessage (getCorrelationId parseResult))\n        else\n            Ok parsed\n\n    let private parseOptionalPromotionSetId (value: string) (parseResult: ParseResult) =\n        if String.IsNullOrWhiteSpace(value) then\n            Ok(Guid.NewGuid())\n        else\n            tryParseGuid value (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult\n\n    let private tryDeserializeDecisionsFromFile (filePath: string) =\n        if not <| File.Exists filePath then\n            Error $\"Decisions file does not exist: {filePath}\"\n        else\n            try\n                let fileContent = File.ReadAllText filePath\n\n                let decisionsResult =\n                    try\n                        Ok(JsonSerializer.Deserialize<ConflictResolutionDecision list>(fileContent, Constants.JsonSerializerOptions))\n                    with\n                    | _ ->\n                        try\n                            let wrapped = JsonSerializer.Deserialize<ConflictDecisionsWrapper>(fileContent, Constants.JsonSerializerOptions)\n                            Ok wrapped.Decisions\n                        with\n                        | _ -> Error \"Decisions file must be valid JSON as an array or { \\\"decisions\\\": [...] }.\"\n\n                match decisionsResult with\n                | Ok decisions when List.isEmpty decisions -> Error \"Decisions file must contain at least one decision.\"\n                | Ok decisions -> Ok decisions\n                | Error error -> Error error\n            with\n            | ex -> Error($\"Unable to read decisions file: {ex.Message}\")\n\n    let private tryDeserializePromotionPointersFromFile (filePath: string) =\n        if not <| File.Exists filePath then\n            Error $\"Promotion pointers file does not exist: {filePath}\"\n        else\n            try\n                let fileContent = File.ReadAllText filePath\n\n                let pointersResult =\n                    try\n                        Ok(JsonSerializer.Deserialize<PromotionPointer list>(fileContent, Constants.JsonSerializerOptions))\n                    with\n                    | _ ->\n                        try\n                            let wrapped = JsonSerializer.Deserialize<PromotionPointersWrapper>(fileContent, Constants.JsonSerializerOptions)\n                            Ok wrapped.PromotionPointers\n                        with\n                        | _ -> Error \"Promotion pointers file must be valid JSON as an array or { \\\"promotionPointers\\\": [...] }.\"\n\n                match pointersResult with\n                | Ok pointers ->\n                    let normalizedPointers = if obj.ReferenceEquals(box pointers, null) then [] else pointers\n\n                    if List.isEmpty normalizedPointers then\n                        Error \"Promotion pointers file must contain at least one entry.\"\n                    else\n                        Ok normalizedPointers\n                | Error error -> Error error\n            with\n            | ex -> Error($\"Unable to read promotion pointers file: {ex.Message}\")\n\n    let private getPromotionSet (graceIds: GraceIds) (promotionSetId: PromotionSetId) =\n        let parameters =\n            Parameters.PromotionSet.GetPromotionSetParameters(\n                PromotionSetId = promotionSetId.ToString(),\n                OwnerId = graceIds.OwnerIdString,\n                OwnerName = graceIds.OwnerName,\n                OrganizationId = graceIds.OrganizationIdString,\n                OrganizationName = graceIds.OrganizationName,\n                RepositoryId = graceIds.RepositoryIdString,\n                RepositoryName = graceIds.RepositoryName,\n                CorrelationId = graceIds.CorrelationId\n            )\n\n        Grace.SDK.PromotionSet.Get(parameters)\n\n    let private getArtifactDownloadUri (graceIds: GraceIds) (artifactId: ArtifactId) =\n        let parameters =\n            Parameters.Artifact.GetArtifactDownloadUriParameters(\n                ArtifactId = artifactId.ToString(),\n                OwnerId = graceIds.OwnerIdString,\n                OwnerName = graceIds.OwnerName,\n                OrganizationId = graceIds.OrganizationIdString,\n                OrganizationName = graceIds.OrganizationName,\n                RepositoryId = graceIds.RepositoryIdString,\n                RepositoryName = graceIds.RepositoryName,\n                CorrelationId = graceIds.CorrelationId\n            )\n\n        Artifact.GetDownloadUri(parameters)\n\n    let private renderPromotionSet (parseResult: ParseResult) (promotionSet: PromotionSetDto) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let table = Table(Border = TableBorder.Rounded)\n            table.AddColumn(\"Field\") |> ignore\n            table.AddColumn(\"Value\") |> ignore\n\n            table.AddRow(\"PromotionSetId\", Markup.Escape(promotionSet.PromotionSetId.ToString()))\n            |> ignore\n\n            table.AddRow(\"TargetBranchId\", Markup.Escape(promotionSet.TargetBranchId.ToString()))\n            |> ignore\n\n            table.AddRow(\"Status\", Markup.Escape(getDiscriminatedUnionCaseName promotionSet.Status))\n            |> ignore\n\n            table.AddRow(\"StepsComputationStatus\", Markup.Escape(getDiscriminatedUnionCaseName promotionSet.StepsComputationStatus))\n            |> ignore\n\n            table.AddRow(\"StepsComputationAttempt\", promotionSet.StepsComputationAttempt.ToString())\n            |> ignore\n\n            table.AddRow(\"StepCount\", promotionSet.Steps.Length.ToString())\n            |> ignore\n\n            AnsiConsole.Write(table)\n\n    let private renderPromotionSetEvents (parseResult: ParseResult) (promotionSetId: PromotionSetId) (events: PromotionSetEvent seq) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let eventsList = events |> Seq.toList\n\n            if List.isEmpty eventsList then\n                AnsiConsole.MarkupLine($\"[yellow]No events found for promotion set[/] {Markup.Escape(promotionSetId.ToString())}.\")\n            else\n                let table = Table(Border = TableBorder.Rounded)\n                table.AddColumn(\"Timestamp\") |> ignore\n                table.AddColumn(\"Event\") |> ignore\n                table.AddColumn(\"Principal\") |> ignore\n\n                eventsList\n                |> List.iter (fun promotionSetEvent ->\n                    table.AddRow(\n                        Markup.Escape(promotionSetEvent.Metadata.Timestamp.ToString()),\n                        Markup.Escape(getDiscriminatedUnionCaseName promotionSetEvent.Event),\n                        Markup.Escape(promotionSetEvent.Metadata.Principal)\n                    )\n                    |> ignore)\n\n                AnsiConsole.Write(table)\n\n    let private buildConflictStepDisplay (graceIds: GraceIds) (step: PromotionSetStep) =\n        task {\n            let baseDisplay =\n                {\n                    StepId = step.StepId\n                    Order = step.Order\n                    ConflictStatus = getDiscriminatedUnionCaseName step.ConflictStatus\n                    ConflictSummaryArtifactId = step.ConflictSummaryArtifactId\n                    DownloadUri = Option.None\n                    DownloadUriError = Option.None\n                }\n\n            match step.ConflictSummaryArtifactId with\n            | Option.None -> return baseDisplay\n            | Option.Some artifactId ->\n                match! getArtifactDownloadUri graceIds artifactId with\n                | Ok returnValue -> return { baseDisplay with DownloadUri = Option.Some returnValue.ReturnValue.DownloadUri }\n                | Error error -> return { baseDisplay with DownloadUriError = Option.Some error.Error }\n        }\n\n    let private renderConflictSummary (parseResult: ParseResult) (result: ConflictShowResult) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            AnsiConsole.MarkupLine($\"[bold]Promotion Set[/] {Markup.Escape(result.PromotionSetId.ToString())}\")\n\n            AnsiConsole.MarkupLine(\n                $\"[bold]Status:[/] {Markup.Escape(result.Status)}  [bold]Computation:[/] {Markup.Escape(result.StepsComputationStatus)}  [bold]Attempt:[/] {result.StepsComputationAttempt}\"\n            )\n\n            if List.isEmpty result.ConflictedSteps then\n                AnsiConsole.MarkupLine(\"[yellow]No conflicts found on the current PromotionSet steps.[/]\")\n            else\n                let table = Table(Border = TableBorder.Rounded)\n\n                table.AddColumns(\n                    [|\n                        \"Order\"\n                        \"StepId\"\n                        \"ConflictStatus\"\n                        \"ArtifactId\"\n                        \"DownloadUri\"\n                    |]\n                )\n                |> ignore\n\n                result.ConflictedSteps\n                |> List.iter (fun step ->\n                    let artifactIdText =\n                        match step.ConflictSummaryArtifactId with\n                        | Option.Some artifactId -> Markup.Escape(artifactId.ToString())\n                        | Option.None -> \"-\"\n\n                    let downloadUriText =\n                        match step.DownloadUri, step.DownloadUriError with\n                        | Option.Some uri, _ -> Markup.Escape(uri.ToString())\n                        | Option.None, Option.Some error -> Markup.Escape($\"Unavailable: {error}\")\n                        | _ -> \"-\"\n\n                    table.AddRow(\n                        step.Order.ToString(),\n                        Markup.Escape(step.StepId.ToString()),\n                        Markup.Escape(step.ConflictStatus),\n                        artifactIdText,\n                        downloadUriText\n                    )\n                    |> ignore)\n\n                AnsiConsole.Write(table)\n\n    let private showConflictsHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    match! getPromotionSet graceIds promotionSetId with\n                    | Error error -> return Error error\n                    | Ok returnValue ->\n                        let promotionSet = returnValue.ReturnValue\n\n                        let conflictedSteps =\n                            promotionSet.Steps\n                            |> List.filter (fun step ->\n                                step.ConflictStatus\n                                <> StepConflictStatus.NoConflicts\n                                || step.ConflictSummaryArtifactId.IsSome)\n\n                        let! displays =\n                            conflictedSteps\n                            |> List.map (buildConflictStepDisplay graceIds)\n                            |> List.toArray\n                            |> Task.WhenAll\n\n                        let result =\n                            {\n                                PromotionSetId = promotionSet.PromotionSetId\n                                Status = getDiscriminatedUnionCaseName promotionSet.Status\n                                StepsComputationStatus = getDiscriminatedUnionCaseName promotionSet.StepsComputationStatus\n                                StepsComputationAttempt = promotionSet.StepsComputationAttempt\n                                ConflictedSteps = displays |> Array.toList\n                            }\n\n                        renderConflictSummary parseResult result\n                        return Ok(GraceReturnValue.Create result graceIds.CorrelationId)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type ShowConflicts() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = showConflictsHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private createPromotionSetHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetIdOptional)\n                let targetBranchIdRaw = parseResult.GetValue(Options.targetBranchId)\n\n                match parseOptionalPromotionSetId promotionSetIdRaw parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    match tryParseGuid targetBranchIdRaw (QueueError.getErrorMessage QueueError.InvalidTargetBranchId) parseResult with\n                    | Error error -> return Error error\n                    | Ok targetBranchId ->\n                        let parameters =\n                            Parameters.PromotionSet.CreatePromotionSetParameters(\n                                PromotionSetId = promotionSetId.ToString(),\n                                TargetBranchId = targetBranchId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result = Grace.SDK.PromotionSet.Create(parameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            if\n                                not (parseResult |> json)\n                                && not (parseResult |> silent)\n                            then\n                                AnsiConsole.MarkupLine(\n                                    $\"[green]Created promotion set[/] {Markup.Escape(promotionSetId.ToString())} [green]for target branch[/] {Markup.Escape(targetBranchId.ToString())}\"\n                                )\n\n                            return Ok returnValue\n                        | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type CreatePromotionSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = createPromotionSetHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private getPromotionSetHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    match! getPromotionSet graceIds promotionSetId with\n                    | Ok returnValue ->\n                        renderPromotionSet parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type GetPromotionSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = getPromotionSetHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private getPromotionSetEventsHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let parameters =\n                        Parameters.PromotionSet.GetPromotionSetEventsParameters(\n                            PromotionSetId = promotionSetId.ToString(),\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Grace.SDK.PromotionSet.GetEvents(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        renderPromotionSetEvents parseResult promotionSetId returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type GetPromotionSetEvents() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = getPromotionSetEventsHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private updateInputPromotionsHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n                let promotionPointersFilePath = parseResult.GetValue(Options.promotionPointersFile)\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    match tryDeserializePromotionPointersFromFile promotionPointersFilePath with\n                    | Error errorText -> return Error(GraceError.Create errorText (getCorrelationId parseResult))\n                    | Ok promotionPointers ->\n                        let parameters =\n                            Parameters.PromotionSet.UpdatePromotionSetInputPromotionsParameters(\n                                PromotionSetId = promotionSetId.ToString(),\n                                PromotionPointers = promotionPointers,\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result = Grace.SDK.PromotionSet.UpdateInputPromotions(parameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            if\n                                not (parseResult |> json)\n                                && not (parseResult |> silent)\n                            then\n                                AnsiConsole.MarkupLine(\n                                    $\"[green]Updated input promotions for promotion set[/] {Markup.Escape(promotionSetId.ToString())} [green]with[/] {promotionPointers.Length} [green]pointer(s).[/]\"\n                                )\n\n                            return Ok returnValue\n                        | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type UpdateInputPromotions() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = updateInputPromotionsHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private recomputePromotionSetHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n\n                let reasonText =\n                    parseResult.GetValue(Options.reason)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let parameters =\n                        Parameters.PromotionSet.RecomputePromotionSetParameters(\n                            PromotionSetId = promotionSetId.ToString(),\n                            Reason = reasonText,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Grace.SDK.PromotionSet.Recompute(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        if\n                            not (parseResult |> json)\n                            && not (parseResult |> silent)\n                        then\n                            AnsiConsole.MarkupLine($\"[green]Requested recompute for promotion set[/] {Markup.Escape(promotionSetId.ToString())}\")\n\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type RecomputePromotionSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = recomputePromotionSetHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private applyPromotionSetHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let parameters =\n                        Parameters.PromotionSet.ApplyPromotionSetParameters(\n                            PromotionSetId = promotionSetId.ToString(),\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Grace.SDK.PromotionSet.Apply(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        if\n                            not (parseResult |> json)\n                            && not (parseResult |> silent)\n                        then\n                            AnsiConsole.MarkupLine($\"[green]Requested apply for promotion set[/] {Markup.Escape(promotionSetId.ToString())}\")\n\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type ApplyPromotionSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = applyPromotionSetHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private deletePromotionSetHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n                let force = parseResult.GetValue(Options.force)\n\n                let deleteReason =\n                    parseResult.GetValue(Options.deleteReason)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let parameters =\n                        Parameters.PromotionSet.DeletePromotionSetParameters(\n                            PromotionSetId = promotionSetId.ToString(),\n                            Force = force,\n                            DeleteReason = deleteReason,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Grace.SDK.PromotionSet.Delete(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        if\n                            not (parseResult |> json)\n                            && not (parseResult |> silent)\n                        then\n                            AnsiConsole.MarkupLine($\"[green]Deleted promotion set[/] {Markup.Escape(promotionSetId.ToString())}\")\n\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type DeletePromotionSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = deletePromotionSetHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private resolveConflictsHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n                let stepIdRaw = parseResult.GetValue(Options.stepId)\n                let decisionsFilePath = parseResult.GetValue(Options.decisionsFile)\n\n                match tryParseGuid promotionSetIdRaw (QueueError.getErrorMessage QueueError.InvalidPromotionSetId) parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    match tryParseGuid stepIdRaw (ValidationResultError.getErrorMessage ValidationResultError.InvalidPromotionSetStepId) parseResult with\n                    | Error error -> return Error error\n                    | Ok stepId ->\n                        match tryDeserializeDecisionsFromFile decisionsFilePath with\n                        | Error errorText -> return Error(GraceError.Create errorText (getCorrelationId parseResult))\n                        | Ok decisions ->\n                            match! getPromotionSet graceIds promotionSetId with\n                            | Error error -> return Error error\n                            | Ok promotionSetReturnValue ->\n                                let promotionSet = promotionSetReturnValue.ReturnValue\n\n                                if promotionSet.StepsComputationAttempt <= 0 then\n                                    return\n                                        Error(\n                                            GraceError.Create\n                                                (ValidationResultError.getErrorMessage ValidationResultError.InvalidStepsComputationAttempt)\n                                                (getCorrelationId parseResult)\n                                        )\n                                else\n                                    let parameters =\n                                        Parameters.PromotionSet.ResolvePromotionSetConflictsParameters(\n                                            PromotionSetId = promotionSetId.ToString(),\n                                            StepId = stepId.ToString(),\n                                            Decisions = decisions,\n                                            StepsComputationAttempt = promotionSet.StepsComputationAttempt,\n                                            OwnerId = graceIds.OwnerIdString,\n                                            OwnerName = graceIds.OwnerName,\n                                            OrganizationId = graceIds.OrganizationIdString,\n                                            OrganizationName = graceIds.OrganizationName,\n                                            RepositoryId = graceIds.RepositoryIdString,\n                                            RepositoryName = graceIds.RepositoryName,\n                                            CorrelationId = graceIds.CorrelationId\n                                        )\n\n                                    let! result = Grace.SDK.PromotionSet.ResolveConflicts(parameters)\n\n                                    match result with\n                                    | Ok returnValue ->\n                                        if\n                                            not (parseResult |> json)\n                                            && not (parseResult |> silent)\n                                        then\n                                            AnsiConsole.MarkupLine(\n                                                $\"[green]Submitted conflict decisions for promotion set[/] {Markup.Escape(promotionSetId.ToString())} [green]step[/] {Markup.Escape(stepId.ToString())}\"\n                                            )\n\n                                        return Ok returnValue\n                                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type ResolveConflicts() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = resolveConflictsHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let promotionSetCommand = new Command(\"promotion-set\", Description = \"Manage promotion sets.\")\n        promotionSetCommand.Aliases.Add(\"prset\")\n        let conflictsCommand = new Command(\"conflicts\", Description = \"Inspect and resolve promotion set conflicts.\")\n\n        let createCommand =\n            new Command(\"create\", Description = \"Create a promotion set for a target branch.\")\n            |> addOption Options.promotionSetIdOptional\n            |> addOption Options.targetBranchId\n            |> addCommonOptions\n\n        createCommand.Action <- new CreatePromotionSet()\n        promotionSetCommand.Subcommands.Add(createCommand)\n\n        let getCommand =\n            new Command(\"get\", Description = \"Get promotion set details.\")\n            |> addOption Options.promotionSetId\n            |> addCommonOptions\n\n        getCommand.Action <- new GetPromotionSet()\n        promotionSetCommand.Subcommands.Add(getCommand)\n\n        let getEventsCommand =\n            new Command(\"get-events\", Description = \"Get promotion set events.\")\n            |> addOption Options.promotionSetId\n            |> addCommonOptions\n\n        getEventsCommand.Action <- new GetPromotionSetEvents()\n        promotionSetCommand.Subcommands.Add(getEventsCommand)\n\n        let updateInputPromotionsCommand =\n            new Command(\"update-input-promotions\", Description = \"Update input promotion pointers for a promotion set.\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.promotionPointersFile\n            |> addCommonOptions\n\n        updateInputPromotionsCommand.Action <- new UpdateInputPromotions()\n        promotionSetCommand.Subcommands.Add(updateInputPromotionsCommand)\n\n        let recomputeCommand =\n            new Command(\"recompute\", Description = \"Request recomputation of promotion set steps.\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.reason\n            |> addCommonOptions\n\n        recomputeCommand.Action <- new RecomputePromotionSet()\n        promotionSetCommand.Subcommands.Add(recomputeCommand)\n\n        let applyCommand =\n            new Command(\"apply\", Description = \"Apply a promotion set.\")\n            |> addOption Options.promotionSetId\n            |> addCommonOptions\n\n        applyCommand.Action <- new ApplyPromotionSet()\n        promotionSetCommand.Subcommands.Add(applyCommand)\n\n        let deleteCommand =\n            new Command(\"delete\", Description = \"Logically delete a promotion set.\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.force\n            |> addOption Options.deleteReason\n            |> addCommonOptions\n\n        deleteCommand.Action <- new DeletePromotionSet()\n        promotionSetCommand.Subcommands.Add(deleteCommand)\n\n        let showConflictsCommand =\n            new Command(\"show\", Description = \"Show conflicts for a promotion set and print conflict artifact URIs.\")\n            |> addOption Options.promotionSetId\n            |> addCommonOptions\n\n        showConflictsCommand.Action <- new ShowConflicts()\n        conflictsCommand.Subcommands.Add(showConflictsCommand)\n\n        let resolveConflictsCommand =\n            new Command(\"resolve\", Description = \"Resolve blocked promotion set conflicts from a decisions JSON file.\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.stepId\n            |> addOption Options.decisionsFile\n            |> addCommonOptions\n\n        resolveConflictsCommand.Action <- new ResolveConflicts()\n        conflictsCommand.Subcommands.Add(resolveConflictsCommand)\n\n        promotionSetCommand.Subcommands.Add(conflictsCommand)\n        promotionSetCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Queue.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Queue\nopen Grace.Types.Types\nopen Spectre.Console\nopen System\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule QueueCommand =\n    module private Options =\n        let promotionSetId =\n            new Option<string>(\n                \"--promotion-set\",\n                [| \"--promotion-set-id\" |],\n                Required = true,\n                Description = \"The promotion set ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let promotionSetIdOptional =\n            new Option<string>(\n                \"--promotion-set\",\n                [| \"--promotion-set-id\" |],\n                Required = false,\n                Description = \"The promotion set ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let workItemId =\n            new Option<string>(\n                \"--work\",\n                [| \"--work-item-id\"; \"-w\" |],\n                Required = false,\n                Description = \"The work item ID <Guid> or work item number <positive integer>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let policySnapshotId =\n            new Option<string>(\n                \"--policy-snapshot-id\",\n                Required = false,\n                Description = \"Policy snapshot ID to initialize the queue.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branch = new Option<string>(\"--branch\", [| \"-b\" |], Required = false, Description = \"Target branch ID or name.\", Arity = ArgumentArity.ExactlyOne)\n\n        let branchId =\n            new Option<BranchId>(\n                OptionName.BranchId,\n                Required = false,\n                Description = \"Target branch ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let branchName =\n            new Option<string>(\n                OptionName.BranchName,\n                Required = false,\n                Description = \"Target branch name. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The organization's name. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                Required = false,\n                Description = \"The repository's name. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    let private tryParseGuid (value: string) (error: QueueError) (parseResult: ParseResult) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value)\n           || Guid.TryParse(value, &parsed) = false\n           || parsed = Guid.Empty then\n            Error(GraceError.Create (QueueError.getErrorMessage error) (getCorrelationId parseResult))\n        else\n            Ok parsed\n\n    let private tryParseWorkItemId (value: string) (parseResult: ParseResult) =\n        let mutable parsedGuid = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value) then\n            Ok String.Empty\n        elif Guid.TryParse(value, &parsedGuid)\n             && parsedGuid <> Guid.Empty then\n            Ok(parsedGuid.ToString())\n        else\n            let mutable parsedNumber = 0L\n\n            if Int64.TryParse(value, &parsedNumber) then\n                if parsedNumber > 0L then\n                    Ok(parsedNumber.ToString())\n                else\n                    Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemNumber) (getCorrelationId parseResult))\n            else\n                Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemId) (getCorrelationId parseResult))\n\n    let private resolveBranchByName (parseResult: ParseResult) (graceIds: GraceIds) (branchName: string) =\n        task {\n            let parameters =\n                Parameters.Branch.GetBranchParameters(\n                    BranchName = branchName,\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            match! Grace.SDK.Branch.Get(parameters) with\n            | Error error -> return Error error\n            | Ok returnValue -> return Ok returnValue.ReturnValue.BranchId\n        }\n\n    let private resolveTargetBranchId (parseResult: ParseResult) (graceIds: GraceIds) =\n        task {\n            let branchRaw =\n                parseResult.GetValue(Options.branch)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            if not (String.IsNullOrWhiteSpace branchRaw) then\n                let mutable parsed = Guid.Empty\n\n                if\n                    Guid.TryParse(branchRaw, &parsed)\n                    && parsed <> Guid.Empty\n                then\n                    return Ok parsed\n                else\n                    return! resolveBranchByName parseResult graceIds branchRaw\n            elif graceIds.BranchId <> Guid.Empty then\n                return Ok graceIds.BranchId\n            elif not (String.IsNullOrWhiteSpace graceIds.BranchName) then\n                return! resolveBranchByName parseResult graceIds graceIds.BranchName\n            else\n                return Error(GraceError.Create (QueueError.getErrorMessage QueueError.InvalidTargetBranchId) (getCorrelationId parseResult))\n        }\n\n    let private resolvePolicySnapshotId (parseResult: ParseResult) (graceIds: GraceIds) (targetBranchId: Guid) =\n        task {\n            let rawPolicySnapshotId =\n                parseResult.GetValue(Options.policySnapshotId)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            if not (String.IsNullOrWhiteSpace rawPolicySnapshotId) then\n                return Ok rawPolicySnapshotId\n            else\n                let parameters =\n                    Parameters.Policy.GetPolicyParameters(\n                        TargetBranchId = targetBranchId.ToString(),\n                        OwnerId = graceIds.OwnerIdString,\n                        OwnerName = graceIds.OwnerName,\n                        OrganizationId = graceIds.OrganizationIdString,\n                        OrganizationName = graceIds.OrganizationName,\n                        RepositoryId = graceIds.RepositoryIdString,\n                        RepositoryName = graceIds.RepositoryName,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                match! Policy.GetCurrent(parameters) with\n                | Error error -> return Error error\n                | Ok returnValue ->\n                    match returnValue.ReturnValue with\n                    | Some snapshot when not (String.IsNullOrWhiteSpace snapshot.PolicySnapshotId) -> return Ok snapshot.PolicySnapshotId\n                    | _ -> return Ok String.Empty\n        }\n\n    let private writeQueueStatus (parseResult: ParseResult) (queue: PromotionQueue) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            let table = Table(Border = TableBorder.Rounded)\n\n            table.AddColumn(TableColumn(\"[bold]Field[/]\").LeftAligned())\n            |> ignore\n\n            table.AddColumn(TableColumn(\"[bold]Value[/]\").LeftAligned())\n            |> ignore\n\n            table.AddRow(\"Target branch\", Markup.Escape(queue.TargetBranchId.ToString()))\n            |> ignore\n\n            table.AddRow(\"State\", Markup.Escape(getDiscriminatedUnionCaseName queue.State))\n            |> ignore\n\n            table.AddRow(\"Promotion set count\", queue.PromotionSetIds.Length.ToString())\n            |> ignore\n\n            match queue.RunningPromotionSetId with\n            | Some promotionSetId ->\n                table.AddRow(\"Running promotion set\", Markup.Escape(promotionSetId.ToString()))\n                |> ignore\n            | None ->\n                table.AddRow(\"Running promotion set\", \"-\")\n                |> ignore\n\n            AnsiConsole.Write(table)\n\n    let private statusHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let! targetBranchIdResult = resolveTargetBranchId parseResult graceIds\n\n                match targetBranchIdResult with\n                | Error error -> return Error error\n                | Ok targetBranchId ->\n                    let parameters =\n                        Parameters.Queue.QueueStatusParameters(\n                            TargetBranchId = targetBranchId.ToString(),\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Queue.Status(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        writeQueueStatus parseResult returnValue.ReturnValue\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Status() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = statusHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private pauseHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let! targetBranchIdResult = resolveTargetBranchId parseResult graceIds\n\n                match targetBranchIdResult with\n                | Error error -> return Error error\n                | Ok targetBranchId ->\n                    let parameters =\n                        Parameters.Queue.QueueActionParameters(\n                            TargetBranchId = targetBranchId.ToString(),\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Queue.Pause(parameters)\n\n                    if\n                        not (parseResult |> json)\n                        && not (parseResult |> silent)\n                    then\n                        AnsiConsole.MarkupLine(\"[green]Queue paused.[/]\")\n\n                    return result\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Pause() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = pauseHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private resumeHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let! targetBranchIdResult = resolveTargetBranchId parseResult graceIds\n\n                match targetBranchIdResult with\n                | Error error -> return Error error\n                | Ok targetBranchId ->\n                    let parameters =\n                        Parameters.Queue.QueueActionParameters(\n                            TargetBranchId = targetBranchId.ToString(),\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = Queue.Resume(parameters)\n\n                    if\n                        not (parseResult |> json)\n                        && not (parseResult |> silent)\n                    then\n                        AnsiConsole.MarkupLine(\"[green]Queue resumed.[/]\")\n\n                    return result\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Resume() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = resumeHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private buildEnqueueParameters (graceIds: GraceIds) (targetBranchId: Guid) (promotionSetId: Guid) (workItemId: string) (policySnapshotId: string) =\n        Parameters.Queue.EnqueueParameters(\n            TargetBranchId = targetBranchId.ToString(),\n            PromotionSetId = promotionSetId.ToString(),\n            WorkItemId = workItemId,\n            PolicySnapshotId = policySnapshotId,\n            OwnerId = graceIds.OwnerIdString,\n            OwnerName = graceIds.OwnerName,\n            OrganizationId = graceIds.OrganizationIdString,\n            OrganizationName = graceIds.OrganizationName,\n            RepositoryId = graceIds.RepositoryIdString,\n            RepositoryName = graceIds.RepositoryName,\n            CorrelationId = graceIds.CorrelationId\n        )\n\n    let private enqueueHandlerImpl (parseResult: ParseResult) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n\n            let promotionSetIdRaw =\n                parseResult.GetValue(Options.promotionSetIdOptional)\n                |> Option.ofObj\n                |> Option.defaultValue (Guid.NewGuid().ToString())\n\n            match tryParseGuid promotionSetIdRaw QueueError.InvalidPromotionSetId parseResult with\n            | Error error -> return Error error\n            | Ok promotionSetId ->\n                let! targetBranchIdResult = resolveTargetBranchId parseResult graceIds\n\n                match targetBranchIdResult with\n                | Error error -> return Error error\n                | Ok targetBranchId ->\n                    let! policySnapshotIdResult = resolvePolicySnapshotId parseResult graceIds targetBranchId\n\n                    match policySnapshotIdResult with\n                    | Error error -> return Error error\n                    | Ok policySnapshotId ->\n                        let workItemIdRaw =\n                            parseResult.GetValue(Options.workItemId)\n                            |> Option.ofObj\n                            |> Option.defaultValue String.Empty\n\n                        match tryParseWorkItemId workItemIdRaw parseResult with\n                        | Error error -> return Error error\n                        | Ok workItemId ->\n                            let parameters = buildEnqueueParameters graceIds targetBranchId promotionSetId workItemId policySnapshotId\n\n                            let! result = Queue.Enqueue(parameters)\n\n                            if\n                                not (parseResult |> json)\n                                && not (parseResult |> silent)\n                            then\n                                AnsiConsole.MarkupLine($\"[green]Enqueued promotion set[/] {Markup.Escape(promotionSetId.ToString())}\")\n\n                            return result\n        }\n\n    let private enqueueHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! enqueueHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Enqueue() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = enqueueHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private dequeueHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let promotionSetIdRaw = parseResult.GetValue(Options.promotionSetId)\n\n                match tryParseGuid promotionSetIdRaw QueueError.InvalidPromotionSetId parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let! targetBranchIdResult = resolveTargetBranchId parseResult graceIds\n\n                    match targetBranchIdResult with\n                    | Error error -> return Error error\n                    | Ok targetBranchId ->\n                        let parameters =\n                            Parameters.Queue.PromotionSetActionParameters(\n                                TargetBranchId = targetBranchId.ToString(),\n                                PromotionSetId = promotionSetId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result = Queue.Dequeue(parameters)\n\n                        if\n                            not (parseResult |> json)\n                            && not (parseResult |> silent)\n                        then\n                            AnsiConsole.MarkupLine($\"[green]Dequeued promotion set[/] {Markup.Escape(promotionSetId.ToString())}\")\n\n                        return result\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Dequeue() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = dequeueHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let addBranchOptions (command: Command) =\n            command\n            |> addOption Options.branch\n            |> addOption Options.branchId\n            |> addOption Options.branchName\n\n        let queueCommand = new Command(\"queue\", Description = \"Manage promotion queues.\")\n\n        let statusCommand =\n            new Command(\"status\", Description = \"Get the status of a promotion queue.\")\n            |> addBranchOptions\n            |> addCommonOptions\n\n        statusCommand.Action <- new Status()\n        queueCommand.Subcommands.Add(statusCommand)\n\n        let enqueueCommand =\n            new Command(\"enqueue\", Description = \"Enqueue a promotion set in a promotion queue.\")\n            |> addOption Options.promotionSetIdOptional\n            |> addOption Options.workItemId\n            |> addOption Options.policySnapshotId\n            |> addBranchOptions\n            |> addCommonOptions\n\n        enqueueCommand.Action <- new Enqueue()\n        queueCommand.Subcommands.Add(enqueueCommand)\n\n        let pauseCommand =\n            new Command(\"pause\", Description = \"Pause a promotion queue.\")\n            |> addBranchOptions\n            |> addCommonOptions\n\n        pauseCommand.Action <- new Pause()\n        queueCommand.Subcommands.Add(pauseCommand)\n\n        let resumeCommand =\n            new Command(\"resume\", Description = \"Resume a promotion queue.\")\n            |> addBranchOptions\n            |> addCommonOptions\n\n        resumeCommand.Action <- new Resume()\n        queueCommand.Subcommands.Add(resumeCommand)\n\n        let dequeueCommand =\n            new Command(\"dequeue\", Description = \"Dequeue a promotion set (admin).\")\n            |> addOption Options.promotionSetId\n            |> addBranchOptions\n            |> addCommonOptions\n\n        dequeueCommand.Action <- new Dequeue()\n        queueCommand.Subcommands.Add(dequeueCommand)\n\n        queueCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Reference.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Parameters.Branch\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Parameters.Storage\nopen Grace.Shared.Services\nopen Grace.Types.Branch\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen NodaTime.TimeZones\nopen Spectre.Console\nopen Spectre.Console.Json\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Globalization\nopen System.IO\nopen System.IO.Enumeration\nopen System.Linq\nopen System.Security.Cryptography\nopen System.Threading\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\n\nmodule Reference =\n    open Grace.Shared.Validation.Common.Input\n\n    module private Options =\n\n        let branchId =\n            new Option<Guid>(\n                OptionName.BranchId,\n                [| \"-i\" |],\n                Required = false,\n                Description = \"The branch's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let branchName =\n            new Option<String>(\n                OptionName.BranchName,\n                [| \"-b\" |],\n                Required = false,\n                Description = \"The name of the branch. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branchNameRequired =\n            new Option<String>(OptionName.BranchName, [| \"-b\" |], Required = true, Description = \"The name of the branch.\", Arity = ArgumentArity.ExactlyOne)\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let parentBranchId =\n            new Option<BranchId>(\n                OptionName.ParentBranchId,\n                [||],\n                Required = false,\n                Description = \"The parent branch's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let parentBranchName =\n            new Option<String>(\n                OptionName.ParentBranchName,\n                [||],\n                Required = false,\n                Description = \"The name of the parent branch. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> String.Empty)\n            )\n\n        let newName = new Option<String>(OptionName.NewName, Required = true, Description = \"The new name of the branch.\", Arity = ArgumentArity.ExactlyOne)\n\n        let message =\n            new Option<String>(\n                OptionName.Message,\n                [| \"-m\" |],\n                Required = false,\n                Description = \"The text to store with this reference.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let messageRequired =\n            new Option<String>(\n                OptionName.Message,\n                [| \"-m\" |],\n                Required = true,\n                Description = \"The text to store with this reference.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let referenceType =\n            (new Option<String>(OptionName.ReferenceType, Required = false, Description = \"The type of reference.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(listCases<ReferenceType> ())\n\n        let fullSha =\n            new Option<bool>(OptionName.FullSha, Required = false, Description = \"Show the full SHA-256 value in output.\", Arity = ArgumentArity.ZeroOrOne)\n\n        let maxCount =\n            new Option<int>(\n                OptionName.MaxCount,\n                Required = false,\n                Description = \"The maximum number of results to return.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> 30)\n            )\n\n        let referenceId =\n            new Option<ReferenceId>(OptionName.ReferenceId, [||], Required = false, Description = \"The reference ID <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n        let sha256Hash =\n            new Option<String>(\n                OptionName.Sha256Hash,\n                [||],\n                Required = false,\n                Description = \"The full or partial SHA-256 hash value of the version.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let enabled =\n            new Option<bool>(\n                OptionName.Enabled,\n                Required = false,\n                Description = \"True to enable the feature; false to disable it.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let includeDeleted =\n            new Option<bool>(OptionName.IncludeDeleted, [| \"-d\" |], Required = false, Description = \"Include deleted branches in the result. [default: false]\")\n\n        let showEvents =\n            new Option<bool>(OptionName.ShowEvents, [| \"-e\" |], Required = false, Description = \"Include actor events in the result. [default: false]\")\n\n        let directoryVersionId =\n            new Option<DirectoryVersionId>(\n                OptionName.DirectoryVersionId,\n                [| \"-v\" |],\n                Required = false,\n                Description = \"The directory version ID to assign to the promotion <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    let private valueOrEmpty (value: string) = if String.IsNullOrWhiteSpace(value) then String.Empty else value\n\n    let private ReferenceValidations (parseResult: ParseResult) =\n        Ok parseResult\n\n    let printContents (parseResult: ParseResult) (directoryVersions: IEnumerable<DirectoryVersion>) =\n        let longestRelativePath =\n            getLongestRelativePath (\n                directoryVersions\n                |> Seq.map (fun directoryVersion -> directoryVersion.ToLocalDirectoryVersion(DateTime.UtcNow))\n            )\n        //logToAnsiConsole Colors.Verbose $\"In printContents: getLongestRelativePath: {longestRelativePath}\"\n        let additionalSpaces = String.replicate (longestRelativePath - 2) \" \"\n        let additionalImportantDashes = String.replicate (longestRelativePath + 3) \"-\"\n        let additionalDeemphasizedDashes = String.replicate (38) \"-\"\n\n        directoryVersions\n        |> Seq.iteri (fun i directoryVersion ->\n            AnsiConsole.WriteLine()\n\n            if i = 0 then\n                AnsiConsole.MarkupLine(\n                    $\"[{Colors.Important}]Created At                   SHA-256            Size  Path{additionalSpaces}[/][{Colors.Deemphasized}] (DirectoryVersionId)[/]\"\n                )\n\n                AnsiConsole.MarkupLine(\n                    $\"[{Colors.Important}]-----------------------------------------------------{additionalImportantDashes}[/][{Colors.Deemphasized}] {additionalDeemphasizedDashes}[/]\"\n                )\n            //logToAnsiConsole Colors.Verbose $\"In printContents: directoryVersion.RelativePath: {directoryVersion.RelativePath}\"\n            let rightAlignedDirectoryVersionId =\n                (String.replicate\n                    (longestRelativePath\n                     - directoryVersion.RelativePath.Length)\n                    \" \")\n                + $\"({directoryVersion.DirectoryVersionId})\"\n\n            AnsiConsole.MarkupLine(\n                $\"[{Colors.Highlighted}]{formatInstantAligned directoryVersion.CreatedAt}   {getShortSha256Hash directoryVersion.Sha256Hash}  {directoryVersion.Size, 13:N0}  /{directoryVersion.RelativePath}[/] [{Colors.Deemphasized}] {rightAlignedDirectoryVersionId}[/]\"\n            )\n            //if parseResult.CommandResult.Command.Options.Contains(Options.listFiles) then\n            let sortedFiles = directoryVersion.Files.OrderBy(fun f -> f.RelativePath)\n\n            for file in sortedFiles do\n                AnsiConsole.MarkupLine(\n                    $\"[{Colors.Verbose}]{formatInstantAligned file.CreatedAt}   {getShortSha256Hash file.Sha256Hash}  {file.Size, 13:N0}  |- {file.RelativePath.Split('/').LastOrDefault()}[/]\"\n                ))\n\n    type GetRecursiveSize() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> CommonValidations\n                        >>= ReferenceValidations\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let referenceId =\n                            if\n                                not\n                                <| isNull (parseResult.GetResult(Options.referenceId))\n                            then\n                                parseResult\n                                    .GetValue(Options.referenceId)\n                                    .ToString()\n                            else\n                                String.Empty\n\n                        let sha256Hash = parseResult.GetValue(Options.sha256Hash)\n\n                        let sdkParameters =\n                            Parameters.Branch.ListContentsParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                Sha256Hash = sha256Hash,\n                                ReferenceId = referenceId,\n                                Pattern = String.Empty,\n                                ShowDirectories = true,\n                                ShowFiles = true,\n                                ForceRecompute = false,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Grace.SDK.Branch.GetRecursiveSize(sdkParameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Grace.SDK.Branch.GetRecursiveSize(sdkParameters)\n\n                        match result with\n                        | Ok returnValue -> AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Total file size: {returnValue.ReturnValue:N0}[/]\"\n                        | Error error -> AnsiConsole.MarkupLine $\"[{Colors.Error}]{error}[/]\"\n\n                        return result |> renderOutput parseResult\n                    | Error error ->\n                        return\n                            GraceResult.Error error\n                            |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type ListContents() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ReferenceValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let referenceId =\n                            if\n                                not\n                                <| isNull (parseResult.GetResult(Options.referenceId))\n                            then\n                                parseResult\n                                    .GetValue(Options.referenceId)\n                                    .ToString()\n                            else\n                                String.Empty\n\n                        let sha256Hash = parseResult.GetValue(Options.sha256Hash)\n\n                        let sdkParameters =\n                            Parameters.Branch.ListContentsParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                Sha256Hash = sha256Hash,\n                                ReferenceId = referenceId,\n                                Pattern = String.Empty,\n                                ShowDirectories = true,\n                                ShowFiles = true,\n                                ForceRecompute = false,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Grace.SDK.Branch.ListContents(sdkParameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Grace.SDK.Branch.ListContents(sdkParameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            let! graceStatus = readGraceStatusFile ()\n\n                            let directoryVersions =\n                                returnValue\n                                    .ReturnValue\n                                    .Select(fun directoryVersionDto -> directoryVersionDto.DirectoryVersion)\n                                    .OrderBy(fun dv -> dv.RelativePath)\n\n                            let directoryCount = directoryVersions.Count()\n\n                            let fileCount =\n                                directoryVersions\n                                    .Select(fun directoryVersion -> directoryVersion.Files.Count)\n                                    .Sum()\n\n                            let totalFileSize = directoryVersions.Sum(fun directoryVersion -> directoryVersion.Files.Sum(fun f -> f.Size))\n\n                            let rootDirectoryVersion = directoryVersions.First(fun d -> d.RelativePath = Constants.RootDirectoryPath)\n\n                            AnsiConsole.MarkupLine($\"[{Colors.Important}]All values taken from the selected version of this branch from the server.[/]\")\n                            AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of directories: {directoryCount}.[/]\")\n                            AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of files: {fileCount}; total file size: {totalFileSize:N0}.[/]\")\n                            AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Root SHA-256 hash: {rootDirectoryVersion.Sha256Hash.Substring(0, 8)}[/]\")\n\n                            printContents parseResult directoryVersions\n                            return result |> renderOutput parseResult\n                        | Error _ -> return result |> renderOutput parseResult\n                    | Error error ->\n                        return\n                            GraceResult.Error error\n                            |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type Assign() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ReferenceValidations\n\n                    let directoryVersionId =\n                        if\n                            not\n                            <| isNull (parseResult.GetResult(Options.directoryVersionId))\n                        then\n                            parseResult.GetValue(Options.directoryVersionId)\n                        else\n                            Guid.Empty\n\n                    let sha256Hash = parseResult.GetValue(Options.sha256Hash)\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        match (directoryVersionId, sha256Hash) with\n                        | (directoryVersionId, sha256Hash) when\n                            directoryVersionId = Guid.Empty\n                            && sha256Hash = String.Empty\n                            ->\n                            let error =\n                                GraceError.Create\n                                    (getErrorMessage ReferenceError.EitherDirectoryVersionIdOrSha256HashRequired)\n                                    (parseResult |> getCorrelationId)\n\n                            return Error error |> renderOutput parseResult\n                        | _ ->\n                            let assignParameters =\n                                Parameters.Branch.AssignParameters(\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    BranchId = graceIds.BranchIdString,\n                                    BranchName = graceIds.BranchName,\n                                    DirectoryVersionId = directoryVersionId,\n                                    Sha256Hash = sha256Hash,\n                                    CorrelationId = getCorrelationId parseResult\n                                )\n\n                            let! result =\n                                if parseResult |> hasOutput then\n                                    progress\n                                        .Columns(progressColumns)\n                                        .StartAsync(fun progressContext ->\n                                            task {\n                                                let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                                let! response = Grace.SDK.Branch.Assign(assignParameters)\n                                                t0.Increment(100.0)\n                                                return response\n                                            })\n                                else\n                                    Grace.SDK.Branch.Assign(assignParameters)\n\n                            return result |> renderOutput parseResult\n                    | Error graceError -> return Error graceError |> renderOutput parseResult\n                with\n                | ex ->\n                    let error = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return Error error |> renderOutput parseResult\n            }\n\n    type CreateReferenceCommand = CreateReferenceParameters -> Task<GraceResult<string>>\n\n    //type ReferenceCommandContext =\n    //    { GraceIds: GraceIds\n    //      OwnerId: string\n    //      OwnerName: string\n    //      OrganizationId: string\n    //      OrganizationName: string\n    //      RepositoryId: string\n    //      RepositoryName: string\n    //      BranchId: string\n    //      BranchName: string\n    //      CorrelationId: string }\n\n    //let buildReferenceContext (parseResult: ParseResult) =\n    //    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n    //    let commonParameters = CommonParameters()\n    //    commonParameters.OwnerId <- graceIds.OwnerIdString |> valueOrEmpty\n    //    commonParameters.OwnerName <- graceIds.OwnerName |> valueOrEmpty\n    //    commonParameters.OrganizationId <- graceIds.OrganizationIdString |> valueOrEmpty\n    //    commonParameters.OrganizationName <- graceIds.OrganizationName |> valueOrEmpty\n    //    commonParameters.RepositoryId <- graceIds.RepositoryIdString |> valueOrEmpty\n    //    commonParameters.RepositoryName <- graceIds.RepositoryName |> valueOrEmpty\n    //    commonParameters.BranchId <- graceIds.BranchIdString |> valueOrEmpty\n    //    commonParameters.BranchName <- graceIds.BranchName |> valueOrEmpty\n\n    //    let (ownerId, organizationId, repositoryId, branchId) = getIds commonParameters\n\n    //    let ownerName =\n    //        if String.IsNullOrWhiteSpace(commonParameters.OwnerName) then\n    //            $\"{Current().OwnerName}\"\n    //        else\n    //            commonParameters.OwnerName\n\n    //    let organizationName =\n    //        if String.IsNullOrWhiteSpace(commonParameters.OrganizationName) then\n    //            $\"{Current().OrganizationName}\"\n    //        else\n    //            commonParameters.OrganizationName\n\n    //    let repositoryName =\n    //        if String.IsNullOrWhiteSpace(commonParameters.RepositoryName) then\n    //            $\"{Current().RepositoryName}\"\n    //        else\n    //            commonParameters.RepositoryName\n\n    //    let branchName =\n    //        if String.IsNullOrWhiteSpace(commonParameters.BranchName) then\n    //            $\"{Current().BranchName}\"\n    //        else\n    //            commonParameters.BranchName\n\n    //    { GraceIds = graceIds\n    //      OwnerId = ownerId\n    //      OwnerName = ownerName\n    //      OrganizationId = organizationId\n    //      OrganizationName = organizationName\n    //      RepositoryId = repositoryId\n    //      RepositoryName = repositoryName\n    //      BranchId = branchId\n    //      BranchName = branchName\n    //      CorrelationId = getCorrelationId parseResult }\n\n    let createReferenceHandler (parseResult: ParseResult) (message: string) (command: CreateReferenceCommand) (commandType: string) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let referenceId =\n                    if\n                        not\n                        <| isNull (parseResult.GetResult(Options.referenceId))\n                    then\n                        parseResult\n                            .GetValue(Options.referenceId)\n                            .ToString()\n                    else\n                        String.Empty\n\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let validateIncomingParameters =\n                    parseResult\n                    |> CommonValidations\n                    >>= ReferenceValidations\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    //let sha256Bytes = SHA256.HashData(Encoding.ASCII.GetBytes(rnd.NextInt64().ToString(\"x8\")))\n                    //let sha256Hash = Seq.fold (fun (sb: StringBuilder) currentByte ->\n                    //    sb.Append(sprintf $\"{currentByte:X2}\")) (StringBuilder(sha256Bytes.Length)) sha256Bytes\n                    if parseResult |> hasOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace status file.[/]\")\n\n                                        let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Scanning working directory for changes.[/]\", autoStart = false)\n\n                                        let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating new directory verions.[/]\", autoStart = false)\n\n                                        let t3 =\n                                            progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading changed files to object storage.[/]\", autoStart = false)\n\n                                        let t4 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Uploading new directory versions.[/]\", autoStart = false)\n\n                                        let t5 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Creating new {commandType}.[/]\", autoStart = false)\n\n                                        //let mutable rootDirectoryId = DirectoryId.Empty\n                                        //let mutable rootDirectorySha256Hash = Sha256Hash String.Empty\n                                        let rootDirectoryVersion = ref (DirectoryVersionId.Empty, Sha256Hash String.Empty)\n\n                                        match! getGraceWatchStatus () with\n                                        | Some graceWatchStatus ->\n                                            t0.Value <- 100.0\n                                            t1.Value <- 100.0\n                                            t2.Value <- 100.0\n                                            t3.Value <- 100.0\n                                            t4.Value <- 100.0\n\n                                            rootDirectoryVersion.Value <- (graceWatchStatus.RootDirectoryId, graceWatchStatus.RootDirectorySha256Hash)\n                                        | None ->\n                                            t0.StartTask() // Read Grace status file.\n                                            let! previousGraceStatus = readGraceStatusFile ()\n                                            let mutable newGraceStatus = previousGraceStatus\n                                            t0.Value <- 100.0\n\n                                            t1.StartTask() // Scan for differences.\n                                            let! differences = scanForDifferences previousGraceStatus\n                                            //logToAnsiConsole Colors.Verbose $\"differences: {serialize differences}\"\n                                            let! newFileVersions = copyUpdatedFilesToObjectCache t1 differences\n                                            //logToAnsiConsole Colors.Verbose $\"newFileVersions: {serialize newFileVersions}\"\n                                            t1.Value <- 100.0\n\n                                            t2.StartTask() // Create new directory versions.\n\n                                            let! (updatedGraceStatus, newDirectoryVersions) =\n                                                getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                                            newGraceStatus <- updatedGraceStatus\n\n                                            rootDirectoryVersion.Value <- (newGraceStatus.RootDirectoryId, newGraceStatus.RootDirectorySha256Hash)\n\n                                            t2.Value <- 100.0\n\n                                            t3.StartTask() // Upload to object storage.\n\n                                            let updatedRelativePaths =\n                                                differences\n                                                    .Select(fun difference ->\n                                                        match difference.DifferenceType with\n                                                        | Add ->\n                                                            match difference.FileSystemEntryType with\n                                                            | FileSystemEntryType.File -> Some difference.RelativePath\n                                                            | FileSystemEntryType.Directory -> None\n                                                        | Change ->\n                                                            match difference.FileSystemEntryType with\n                                                            | FileSystemEntryType.File -> Some difference.RelativePath\n                                                            | FileSystemEntryType.Directory -> None\n                                                        | Delete -> None)\n                                                    .Where(fun relativePathOption -> relativePathOption.IsSome)\n                                                    .Select(fun relativePath -> relativePath.Value)\n\n                                            // let newFileVersions = updatedRelativePaths.Select(fun relativePath ->\n                                            //     newDirectoryVersions.First(fun dv -> dv.Files.Exists(fun file -> file.RelativePath = relativePath)).Files.First(fun file -> file.RelativePath = relativePath))\n\n                                            let mutable lastFileUploadInstant = newGraceStatus.LastSuccessfulFileUpload\n\n                                            if newFileVersions.Count() > 0 then\n                                                let getUploadMetadataForFilesParameters =\n                                                    GetUploadMetadataForFilesParameters(\n                                                        OwnerId = graceIds.OwnerIdString,\n                                                        OwnerName = graceIds.OwnerName,\n                                                        OrganizationId = graceIds.OrganizationIdString,\n                                                        OrganizationName = graceIds.OrganizationName,\n                                                        RepositoryId = graceIds.RepositoryIdString,\n                                                        RepositoryName = graceIds.RepositoryName,\n                                                        CorrelationId = graceIds.CorrelationId,\n                                                        FileVersions =\n                                                            (newFileVersions\n                                                             |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                                             |> Seq.toArray)\n                                                    )\n\n                                                match! uploadFilesToObjectStorage getUploadMetadataForFilesParameters with\n                                                | Ok returnValue -> () //logToAnsiConsole Colors.Verbose $\"Uploaded all files to object storage.\"\n                                                | Error error -> logToAnsiConsole Colors.Error $\"Error uploading files to object storage: {error.Error}\"\n\n                                                lastFileUploadInstant <- getCurrentInstant ()\n\n                                            t3.Value <- 100.0\n\n                                            t4.StartTask() // Upload directory versions.\n\n                                            let mutable lastDirectoryVersionUpload = newGraceStatus.LastSuccessfulDirectoryVersionUpload\n\n                                            if newDirectoryVersions.Count > 0 then\n                                                let saveParameters = SaveDirectoryVersionsParameters()\n                                                saveParameters.OwnerId <- graceIds.OwnerIdString\n                                                saveParameters.OwnerName <- graceIds.OwnerName\n                                                saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                                                saveParameters.OrganizationName <- graceIds.OrganizationName\n                                                saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                                                saveParameters.RepositoryName <- graceIds.RepositoryName\n                                                saveParameters.CorrelationId <- graceIds.CorrelationId\n                                                saveParameters.DirectoryVersionId <- $\"{newGraceStatus.RootDirectoryId}\"\n\n                                                saveParameters.DirectoryVersions <-\n                                                    newDirectoryVersions\n                                                        .Select(fun dv -> dv.ToDirectoryVersion)\n                                                        .ToList()\n\n                                                let! uploadDirectoryVersions = DirectoryVersion.SaveDirectoryVersions saveParameters\n\n                                                lastDirectoryVersionUpload <- getCurrentInstant ()\n\n                                            t4.Value <- 100.0\n\n                                            newGraceStatus <-\n                                                { newGraceStatus with\n                                                    LastSuccessfulFileUpload = lastFileUploadInstant\n                                                    LastSuccessfulDirectoryVersionUpload = lastDirectoryVersionUpload\n                                                }\n\n                                            do! applyGraceStatusIncremental newGraceStatus newDirectoryVersions differences\n\n                                        t5.StartTask() // Create new reference.\n\n                                        let (rootDirectoryId, rootDirectorySha256Hash) = rootDirectoryVersion.Value\n\n                                        let sdkParameters =\n                                            Parameters.Branch.CreateReferenceParameters(\n                                                BranchId = graceIds.BranchIdString,\n                                                BranchName = graceIds.BranchName,\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                DirectoryVersionId = rootDirectoryId,\n                                                Sha256Hash = rootDirectorySha256Hash,\n                                                Message = message,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        let! result = command sdkParameters\n                                        t5.Value <- 100.0\n\n                                        return result\n                                    })\n                    else\n                        let! previousGraceStatus = readGraceStatusFile ()\n                        let! differences = scanForDifferences previousGraceStatus\n\n                        let! (newGraceIndex, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions previousGraceStatus differences\n\n                        let updatedRelativePaths =\n                            differences\n                                .Select(fun difference ->\n                                    match difference.DifferenceType with\n                                    | Add ->\n                                        match difference.FileSystemEntryType with\n                                        | FileSystemEntryType.File -> Some difference.RelativePath\n                                        | FileSystemEntryType.Directory -> None\n                                    | Change ->\n                                        match difference.FileSystemEntryType with\n                                        | FileSystemEntryType.File -> Some difference.RelativePath\n                                        | FileSystemEntryType.Directory -> None\n                                    | Delete -> None)\n                                .Where(fun relativePathOption -> relativePathOption.IsSome)\n                                .Select(fun relativePath -> relativePath.Value)\n\n                        let newFileVersions =\n                            updatedRelativePaths.Select (fun relativePath ->\n                                newDirectoryVersions\n                                    .First(fun dv -> dv.Files.Exists(fun file -> file.RelativePath = relativePath))\n                                    .Files.First(fun file -> file.RelativePath = relativePath))\n\n                        let getUploadMetadataForFilesParameters =\n                            GetUploadMetadataForFilesParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId,\n                                FileVersions =\n                                    (newFileVersions\n                                     |> Seq.map (fun localFileVersion -> localFileVersion.ToFileVersion)\n                                     |> Seq.toArray)\n                            )\n\n                        let! uploadResult = uploadFilesToObjectStorage getUploadMetadataForFilesParameters\n                        let saveParameters = SaveDirectoryVersionsParameters()\n                        saveParameters.OwnerId <- graceIds.OwnerIdString\n                        saveParameters.OwnerName <- graceIds.OwnerName\n                        saveParameters.OrganizationId <- graceIds.OrganizationIdString\n                        saveParameters.OrganizationName <- graceIds.OrganizationName\n                        saveParameters.RepositoryId <- graceIds.RepositoryIdString\n                        saveParameters.RepositoryName <- graceIds.RepositoryName\n                        saveParameters.CorrelationId <- graceIds.CorrelationId\n\n                        saveParameters.DirectoryVersions <-\n                            newDirectoryVersions\n                                .Select(fun dv -> dv.ToDirectoryVersion)\n                                .ToList()\n\n                        let! uploadDirectoryVersions = DirectoryVersion.SaveDirectoryVersions saveParameters\n                        let rootDirectoryVersion = getRootDirectoryVersion previousGraceStatus\n\n                        let sdkParameters =\n                            Parameters.Branch.CreateReferenceParameters(\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                DirectoryVersionId = rootDirectoryVersion.DirectoryVersionId,\n                                Sha256Hash = rootDirectoryVersion.Sha256Hash,\n                                Message = message,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result = command sdkParameters\n                        return result\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    let promotionHandler (parseResult: ParseResult) (message: string) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = parseResult |> ReferenceValidations\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    if parseResult |> hasOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading Grace status file.[/]\")\n\n                                        let t1 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Checking if the promotion is valid.[/]\", autoStart = false)\n\n                                        let t2 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\", autoStart = false)\n\n                                        // Read Grace status file.\n                                        let! graceStatus = readGraceStatusFile ()\n                                        let rootDirectoryId = graceStatus.RootDirectoryId\n                                        let rootDirectorySha256Hash = graceStatus.RootDirectorySha256Hash\n                                        t0.Value <- 100.0\n\n                                        // Check if the promotion is valid; i.e. it's allowed by the ReferenceTypes enabled in the repository.\n                                        t1.StartTask()\n                                        // For single-step promotion, the current branch's latest commit will become the parent branch's next promotion.\n                                        // If our current state is not the latest commit, print a warning message.\n\n                                        // Get the Dto for the current branch. That will have its latest commit.\n                                        let branchGetParameters =\n                                            GetBranchParameters(\n                                                BranchId = graceIds.BranchIdString,\n                                                BranchName = graceIds.BranchName,\n                                                OwnerId = graceIds.OwnerIdString,\n                                                OwnerName = graceIds.OwnerName,\n                                                OrganizationId = graceIds.OrganizationIdString,\n                                                OrganizationName = graceIds.OrganizationName,\n                                                RepositoryId = graceIds.RepositoryIdString,\n                                                RepositoryName = graceIds.RepositoryName,\n                                                CorrelationId = graceIds.CorrelationId\n                                            )\n\n                                        let! branchResult = Grace.SDK.Branch.Get(branchGetParameters)\n\n                                        match branchResult with\n                                        | Ok branchReturnValue ->\n                                            // If we succeeded, get the parent branch Dto. That will have its latest promotion.\n                                            let! parentBranchResult = Branch.GetParentBranch(branchGetParameters)\n\n                                            match parentBranchResult with\n                                            | Ok parentBranchReturnValue ->\n                                                // Yay, we have both Dto's.\n                                                let branchDto = branchReturnValue.ReturnValue\n                                                let parentBranchDto = parentBranchReturnValue.ReturnValue\n\n                                                // Get the references for the latest commit and/or promotion on the current branch.\n                                                //let getReferenceParameters =\n                                                //    Parameters.Branch.GetReferenceParameters(BranchId = parameters.BranchId, BranchName = parameters.BranchName,\n                                                //        OwnerId = parameters.OwnerId, OwnerName = parameters.OwnerName,\n                                                //        OrganizationId = parameters.OrganizationId, OrganizationName = parameters.OrganizationName,\n                                                //        RepositoryId = parameters.RepositoryId, RepositoryName = parameters.RepositoryName,\n                                                //        ReferenceId = $\"{branchDto.LatestCommit}\", CorrelationId = parameters.CorrelationId)\n                                                //let! referenceResult = Branch.GetReference(getReferenceParameters)\n\n                                                let referenceIds = List<ReferenceId>()\n\n                                                if branchDto.LatestCommit <> ReferenceDto.Default then\n                                                    referenceIds.Add(branchDto.LatestCommit.ReferenceId)\n\n                                                if branchDto.LatestPromotion <> ReferenceDto.Default then\n                                                    referenceIds.Add(branchDto.LatestPromotion.ReferenceId)\n\n                                                if referenceIds.Count > 0 then\n                                                    let getReferencesByReferenceIdParameters =\n                                                        Parameters.Repository.GetReferencesByReferenceIdParameters(\n                                                            OwnerId = graceIds.OwnerIdString,\n                                                            OwnerName = graceIds.OwnerName,\n                                                            OrganizationId = graceIds.OrganizationIdString,\n                                                            OrganizationName = graceIds.OrganizationName,\n                                                            RepositoryId = graceIds.RepositoryIdString,\n                                                            RepositoryName = graceIds.RepositoryName,\n                                                            ReferenceIds = referenceIds,\n                                                            CorrelationId = graceIds.CorrelationId\n                                                        )\n\n                                                    match! Repository.GetReferencesByReferenceId(getReferencesByReferenceIdParameters) with\n                                                    | Ok returnValue ->\n                                                        let references = returnValue.ReturnValue\n\n                                                        let latestPromotableReference =\n                                                            references\n                                                                .OrderByDescending(fun reference -> reference.CreatedAt)\n                                                                .First()\n                                                        // If the current branch's latest reference is not the latest commit - i.e. they've done more work in the branch\n                                                        //   after the commit they're expecting to promote - print a warning.\n                                                        //match getReferencesByReferenceIdResult with\n                                                        //| Ok returnValue ->\n                                                        //    let references = returnValue.ReturnValue\n                                                        //    if referenceDto.DirectoryId <> graceStatus.RootDirectoryId then\n                                                        //        logToAnsiConsole Colors.Important $\"Note: the branch has been updated since the latest commit.\"\n                                                        //| Error error -> () // I don't really care if this call fails, it's just a warning message.\n                                                        t1.Value <- 100.0\n\n                                                        // If the current branch is based on the parent's latest promotion, then we can proceed with the promotion.\n                                                        if branchDto.BasedOn.ReferenceId = parentBranchDto.LatestPromotion.ReferenceId then\n                                                            t2.StartTask()\n\n                                                            let promotionParameters =\n                                                                Parameters.Branch.CreateReferenceParameters(\n                                                                    BranchId = $\"{parentBranchDto.BranchId}\",\n                                                                    OwnerId = graceIds.OwnerIdString,\n                                                                    OwnerName = graceIds.OwnerName,\n                                                                    OrganizationId = graceIds.OrganizationIdString,\n                                                                    OrganizationName = graceIds.OrganizationName,\n                                                                    RepositoryId = graceIds.RepositoryIdString,\n                                                                    RepositoryName = graceIds.RepositoryName,\n                                                                    DirectoryVersionId = latestPromotableReference.DirectoryId,\n                                                                    Sha256Hash = latestPromotableReference.Sha256Hash,\n                                                                    Message = message,\n                                                                    CorrelationId = graceIds.CorrelationId\n                                                                )\n\n                                                            let! promotionResult = Grace.SDK.Branch.Promote(promotionParameters)\n\n                                                            match promotionResult with\n                                                            | Ok returnValue ->\n                                                                logToAnsiConsole Colors.Verbose $\"Succeeded doing promotion.\"\n\n                                                                let promotionReferenceId = Guid.Parse(returnValue.Properties[\"ReferenceId\"] :?> string)\n\n                                                                let rebaseParameters =\n                                                                    Parameters.Branch.RebaseParameters(\n                                                                        BranchId = $\"{branchDto.BranchId}\",\n                                                                        RepositoryId = $\"{branchDto.RepositoryId}\",\n                                                                        OwnerId = graceIds.OwnerIdString,\n                                                                        OwnerName = graceIds.OwnerName,\n                                                                        OrganizationId = graceIds.OrganizationIdString,\n                                                                        OrganizationName = graceIds.OrganizationName,\n                                                                        BasedOn = promotionReferenceId\n                                                                    )\n\n                                                                let! rebaseResult = Grace.SDK.Branch.Rebase(rebaseParameters)\n                                                                t2.Value <- 100.0\n\n                                                                match rebaseResult with\n                                                                | Ok returnValue ->\n                                                                    logToAnsiConsole Colors.Verbose $\"Succeeded doing rebase.\"\n\n                                                                    return promotionResult\n                                                                | Error error -> return Error error\n                                                            | Error error ->\n                                                                t2.Value <- 100.0\n                                                                return Error error\n\n                                                        else\n                                                            return\n                                                                Error(\n                                                                    GraceError.Create\n                                                                        (getErrorMessage BranchError.BranchIsNotBasedOnLatestPromotion)\n                                                                        (parseResult |> getCorrelationId)\n                                                                )\n                                                    | Error error ->\n                                                        t2.Value <- 100.0\n                                                        return Error error\n                                                else\n                                                    return\n                                                        Error(\n                                                            GraceError.Create\n                                                                (getErrorMessage ReferenceError.PromotionNotAvailableBecauseThereAreNoPromotableReferences)\n                                                                (parseResult |> getCorrelationId)\n                                                        )\n                                            | Error error ->\n                                                t1.Value <- 100.0\n                                                return Error error\n                                        | Error error ->\n                                            t1.Value <- 100.0\n                                            return Error error\n                                    })\n                    else\n                        // Same result, with no output.\n                        return Error(GraceError.Create \"Need to implement the else clause.\" (parseResult |> getCorrelationId))\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    type Promote() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let message =\n                    parseResult.GetValue(Options.message)\n                    |> valueOrEmpty\n\n                let! result = promotionHandler parseResult message\n                return result |> renderOutput parseResult\n            }\n\n    type Commit() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let message =\n                    parseResult.GetValue(Options.messageRequired)\n                    |> valueOrEmpty\n\n                let command (parameters: CreateReferenceParameters) = task { return! Grace.SDK.Branch.Commit(parameters) }\n\n                let! result = createReferenceHandler parseResult message command (nameof(Commit).ToLowerInvariant())\n\n                return result |> renderOutput parseResult\n            }\n\n    type Checkpoint() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let message =\n                    parseResult.GetValue(Options.message)\n                    |> valueOrEmpty\n\n                let command (parameters: CreateReferenceParameters) = task { return! Grace.SDK.Branch.Checkpoint(parameters) }\n\n                let! result = createReferenceHandler parseResult message command (nameof(Checkpoint).ToLowerInvariant())\n\n                return result |> renderOutput parseResult\n            }\n\n    type Save() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let message =\n                    parseResult.GetValue(Options.message)\n                    |> valueOrEmpty\n\n                let command (parameters: CreateReferenceParameters) = task { return! Grace.SDK.Branch.Save(parameters) }\n\n                let! result = createReferenceHandler parseResult message command (nameof(Save).ToLowerInvariant())\n\n                return result |> renderOutput parseResult\n            }\n\n    type Tag() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let message =\n                    parseResult.GetValue(Options.messageRequired)\n                    |> valueOrEmpty\n\n                let command (parameters: CreateReferenceParameters) = task { return! Grace.SDK.Branch.Tag(parameters) }\n\n                let! result = createReferenceHandler parseResult message command (nameof(Tag).ToLowerInvariant())\n\n                return result |> renderOutput parseResult\n            }\n\n    type CreateExternal() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let message =\n                    parseResult.GetValue(Options.messageRequired)\n                    |> valueOrEmpty\n\n                let command (parameters: CreateReferenceParameters) = task { return! Grace.SDK.Branch.CreateExternal(parameters) }\n\n                let! result = createReferenceHandler parseResult message command (\"External\".ToLowerInvariant())\n\n                return result |> renderOutput parseResult\n            }\n\n    type Get() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let includeDeleted = parseResult.GetValue(Options.includeDeleted)\n                    let showEvents = parseResult.GetValue(Options.showEvents)\n                    let validateIncomingParameters = parseResult |> ReferenceValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let branchParameters =\n                            GetBranchParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                IncludeDeleted = includeDeleted,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Grace.SDK.Branch.Get(branchParameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Grace.SDK.Branch.Get(branchParameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                            AnsiConsole.Write(jsonText)\n                            AnsiConsole.WriteLine()\n\n                            if showEvents then\n                                let eventParameters =\n                                    GetBranchVersionParameters(\n                                        OwnerId = graceIds.OwnerIdString,\n                                        OwnerName = graceIds.OwnerName,\n                                        OrganizationId = graceIds.OrganizationIdString,\n                                        OrganizationName = graceIds.OrganizationName,\n                                        RepositoryId = graceIds.RepositoryIdString,\n                                        RepositoryName = graceIds.RepositoryName,\n                                        BranchId = graceIds.BranchIdString,\n                                        BranchName = graceIds.BranchName,\n                                        IncludeDeleted = includeDeleted,\n                                        CorrelationId = getCorrelationId parseResult\n                                    )\n\n                                let! eventsResult =\n                                    if parseResult |> hasOutput then\n                                        progress\n                                            .Columns(progressColumns)\n                                            .StartAsync(fun progressContext ->\n                                                task {\n                                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                                    let! response = Branch.GetEvents(eventParameters)\n                                                    t0.Increment(100.0)\n                                                    return response\n                                                })\n                                    else\n                                        Branch.GetEvents(eventParameters)\n\n                                match eventsResult with\n                                | Ok eventsValue ->\n                                    for line in eventsValue.ReturnValue do\n                                        AnsiConsole.MarkupLine $\"[{Colors.Verbose}]{Markup.Escape(line)}[/]\"\n\n                                    AnsiConsole.WriteLine()\n                                    return 0\n                                | Error eventError ->\n                                    return\n                                        GraceResult.Error eventError\n                                        |> renderOutput parseResult\n                            else\n                                return 0\n                        | Error graceError ->\n                            return\n                                GraceResult.Error graceError\n                                |> renderOutput parseResult\n                    | Error graceError ->\n                        return\n                            GraceResult.Error graceError\n                            |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    type Delete() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n                    let validateIncomingParameters = parseResult |> ReferenceValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let deleteParameters =\n                            Parameters.Branch.DeleteBranchParameters(\n                                BranchId = graceIds.BranchIdString,\n                                BranchName = graceIds.BranchName,\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Grace.SDK.Branch.Delete(deleteParameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Grace.SDK.Branch.Delete(deleteParameters)\n\n                        return result |> renderOutput parseResult\n                    | Error graceError ->\n                        return\n                            GraceResult.Error graceError\n                            |> renderOutput parseResult\n                with\n                | ex ->\n                    let graceError = GraceError.Create $\"{ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)\n\n                    return renderOutput parseResult (GraceResult.Error graceError)\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n            |> addOption Options.branchName\n            |> addOption Options.branchId\n\n        // Create main command and aliases, if any.`\n        let referenceCommand = new Command(\"reference\", Description = \"Create or delete references.\")\n\n        referenceCommand.Aliases.Add(\"ref\")\n\n        let promoteCommand =\n            new Command(\"promote\", Description = \"Promotes a commit into the parent branch.\")\n            |> addOption Options.message\n            |> addCommonOptions\n\n        promoteCommand.Action <- new Promote()\n        referenceCommand.Subcommands.Add(promoteCommand)\n\n        let commitCommand =\n            new Command(\"commit\", Description = \"Create a commit.\")\n            |> addOption Options.messageRequired\n            |> addCommonOptions\n\n        commitCommand.Action <- new Commit()\n        referenceCommand.Subcommands.Add(commitCommand)\n\n        let checkpointCommand =\n            new Command(\"checkpoint\", Description = \"Create a checkpoint.\")\n            |> addOption Options.message\n            |> addCommonOptions\n\n        checkpointCommand.Action <- new Checkpoint()\n        referenceCommand.Subcommands.Add(checkpointCommand)\n\n        let saveCommand =\n            new Command(\"save\", Description = \"Create a save.\")\n            |> addOption Options.message\n            |> addCommonOptions\n\n        saveCommand.Action <- new Save()\n        referenceCommand.Subcommands.Add(saveCommand)\n\n        let tagCommand =\n            new Command(\"tag\", Description = \"Create a tag.\")\n            |> addOption Options.messageRequired\n            |> addCommonOptions\n\n        tagCommand.Action <- new Tag()\n        referenceCommand.Subcommands.Add(tagCommand)\n\n        let createExternalCommand =\n            new Command(\"create-external\", Description = \"Create an external reference.\")\n            |> addOption Options.messageRequired\n            |> addCommonOptions\n\n        createExternalCommand.Action <- new CreateExternal()\n        referenceCommand.Subcommands.Add(createExternalCommand)\n\n        let getCommand =\n            new Command(\"get\", Description = \"Gets details for the branch.\")\n            |> addOption Options.includeDeleted\n            |> addOption Options.showEvents\n            |> addCommonOptions\n\n        getCommand.Action <- new Get()\n        referenceCommand.Subcommands.Add(getCommand)\n\n        let deleteCommand =\n            new Command(\"delete\", Description = \"Delete the branch.\")\n            |> addCommonOptions\n\n        deleteCommand.Action <- new Delete()\n        referenceCommand.Subcommands.Add(deleteCommand)\n\n        let assignCommand =\n            new Command(\"assign\", Description = \"Assign a promotion to this branch.\")\n            |> addOption Options.directoryVersionId\n            |> addOption Options.sha256Hash\n            |> addOption Options.message\n            |> addCommonOptions\n\n        assignCommand.Action <- new Assign()\n        referenceCommand.Subcommands.Add(assignCommand)\n\n        //let undeleteCommand = new Command(\"undelete\", Description = \"Undelete a deleted owner.\") |> addCommonOptions\n        //undeleteCommand.Action <- Undelete\n        //branchCommand.Subcommands.Add(undeleteCommand)\n\n        referenceCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Repository.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen FSharpPlus\nopen Grace.CLI.Common\nopen Grace.CLI.Common.Validations\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Parameters\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Parameters.Repository\nopen Grace.Shared.Parameters.Storage\nopen Grace.Shared.Validation.Errors\nopen NodaTime\nopen Spectre.Console\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.Linq\nopen System.IO\nopen System.Threading\nopen System.Threading.Tasks\nopen Spectre.Console.Json\n\nmodule Repository =\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let requiredRepositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = true,\n                Description = \"The name of the repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let description =\n            new Option<String>(OptionName.Description, Required = false, Description = \"The description of the repository.\", Arity = ArgumentArity.ExactlyOne)\n\n        let visibility =\n            (new Option<RepositoryType>(\n                OptionName.Visibility,\n                Required = true,\n                Description = \"The visibility of the repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<RepositoryType> ())\n\n        let status =\n            (new Option<String>(OptionName.Status, Required = true, Description = \"The status of the repository.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(listCases<RepositoryStatus> ())\n\n        let recordSaves =\n            new Option<bool>(\n                OptionName.RecordSaves,\n                Required = true,\n                Description = \"True to record all saves; false to turn it off.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let defaultServerApiVersion =\n            (new Option<String>(\n                OptionName.DefaultServerApiVersion,\n                Required = true,\n                Description = \"The default version of the server API that clients should use when accessing this repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<ServerApiVersions> ())\n\n        let saveDays =\n            new Option<single>(\n                OptionName.SaveDays,\n                Required = true,\n                Description = \"How many days to keep saves. [default: 7.0]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let checkpointDays =\n            new Option<single>(\n                OptionName.CheckpointDays,\n                Required = true,\n                Description = \"How many days to keep checkpoints. [default: 365.0]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let diffCacheDays =\n            new Option<single>(\n                OptionName.DiffCacheDays,\n                Required = true,\n                Description = \"How many days to keep diff results cached in the database. [default: 3.0]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let directoryVersionCacheDays =\n            new Option<single>(\n                OptionName.DirectoryVersionCacheDays,\n                Required = true,\n                Description = \"How many days to keep recursive directory version contents cached. [default: 3.0]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let logicalDeleteDays =\n            new Option<single>(\n                OptionName.LogicalDeleteDays,\n                Required = true,\n                Description = \"How many days to keep deleted branches before permanently deleting them. [default: 30.0]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let newName =\n            new Option<String>(OptionName.NewName, Required = true, Description = \"The new name for the repository.\", Arity = ArgumentArity.ExactlyOne)\n\n        let deleteReason =\n            new Option<String>(\n                OptionName.DeleteReason,\n                Required = true,\n                Description = \"The reason for deleting the repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let graceConfig =\n            new Option<String>(\n                OptionName.GraceConfig,\n                Required = false,\n                Description = \"The path of a Grace config file that you'd like to use instead of the default graceconfig.json.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let force =\n            new Option<bool>(\n                OptionName.Force,\n                Required = false,\n                Description = \"Deletes repository even if there are links to other repositories.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let doNotSwitch =\n            new Option<bool>(\n                OptionName.DoNotSwitch,\n                Required = false,\n                Description =\n                    \"Do not switch your current repository to the new repository after it is created. By default, the new repository becomes the current repository.\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let directory =\n            new Option<String>(\n                OptionName.Directory,\n                Required = false,\n                Description = \"The directory to use when initializing the repository. [default: current directory]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let includeDeleted =\n            new Option<bool>(\n                OptionName.IncludeDeleted,\n                [| \"-d\" |],\n                Required = false,\n                Description = \"Include deleted branches in the result.\",\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let anonymousAccess =\n            new Option<bool>(\n                OptionName.AnonymousAccess,\n                Required = true,\n                Description = \"Enable or disable anonymous access for the repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let allowsLargeFiles =\n            new Option<bool>(\n                OptionName.AllowsLargeFiles,\n                Required = true,\n                Description = \"Enable or disable large file support for the repository.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let conflictResolutionPolicy =\n            (new Option<String>(\n                \"--conflict-resolution-policy\",\n                Required = true,\n                Description = \"The repository's resolution conflict policy when conflicts are detected in a PromotionSet.\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong(listCases<ConflictResolutionPolicy> ())\n\n        let confidenceThreshold =\n            new Option<float32>(\n                \"--confidence-threshold\",\n                Required = false,\n                Description =\n                    \"The confidence threshold for auto-accepting conflict resolutions (0.0 to 1.0). Required when policy is ConflictsAllowedWithConfidence.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> 0.8f)\n            )\n\n        confidenceThreshold.Validators.Add (fun optionResult ->\n            let parseResult = optionResult.GetValueOrDefault<float32>()\n\n            if parseResult < 0.0f || parseResult > 1.0f then\n                optionResult.AddError(\"The confidence threshold must be between 0.0 and 1.0.\"))\n\n    // Create subcommand.\n    type Create() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    // In a Create() command, if --repository-id is implicit, that's actually the old RepositoryId taken from graceconfig.json,\n                    //   and we need to set RepositoryId to a new Guid.\n                    let mutable graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    if parseResult.GetResult(Options.ownerId).Implicit then\n                        let repositoryId = Guid.NewGuid()\n                        graceIds <- { graceIds with RepositoryId = repositoryId; RepositoryIdString = $\"{repositoryId}\" }\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let repositoryIdOption = parseResult.GetResult(Options.repositoryId)\n\n                        let repositoryId =\n                            if isNull repositoryIdOption\n                               || repositoryIdOption.Implicit then\n                                Guid.NewGuid().ToString()\n                            else\n                                graceIds.RepositoryIdString\n\n                        let ownerId = if graceIds.HasOwner then graceIds.OwnerIdString else $\"{Current().OwnerId}\"\n\n                        let organizationId =\n                            if graceIds.HasOrganization then\n                                graceIds.OrganizationIdString\n                            else\n                                $\"{Current().OrganizationId}\"\n\n                        let parameters =\n                            Repository.CreateRepositoryParameters(\n                                RepositoryId = repositoryId,\n                                RepositoryName = graceIds.RepositoryName,\n                                OwnerId = ownerId,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = organizationId,\n                                OrganizationName = graceIds.OrganizationName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        if parseResult |> hasOutput then\n                            let! result =\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! result = Repository.Create(parameters)\n                                            t0.Increment(100.0)\n                                            return result\n                                        })\n\n                            match result with\n                            | Ok returnValue ->\n                                if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                    let newConfig = Current()\n                                    newConfig.RepositoryId <- Guid.Parse($\"{returnValue.Properties[nameof RepositoryId]}\")\n                                    newConfig.RepositoryName <- $\"{returnValue.Properties[nameof RepositoryName]}\"\n                                    newConfig.BranchId <- Guid.Parse($\"{returnValue.Properties[nameof BranchId]}\")\n                                    newConfig.BranchName <- $\"{returnValue.Properties[nameof BranchName]}\"\n                                    newConfig.DefaultBranchName <- \"main\"\n                                    newConfig.ObjectStorageProvider <- ObjectStorageProvider.AzureBlobStorage\n                                    updateConfiguration newConfig\n\n                                return result |> renderOutput parseResult\n                            | Error error ->\n                                logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                                return result |> renderOutput parseResult\n                        else\n                            let! result = Repository.Create(parameters)\n\n                            match result with\n                            | Ok returnValue ->\n                                if not <| parseResult.GetValue(Options.doNotSwitch) then\n                                    let newConfig = Current()\n                                    newConfig.RepositoryId <- Guid.Parse($\"{returnValue.Properties[nameof RepositoryId]}\")\n                                    newConfig.RepositoryName <- $\"{returnValue.Properties[nameof RepositoryName]}\"\n                                    newConfig.BranchId <- Guid.Parse($\"{returnValue.Properties[nameof BranchId]}\")\n                                    newConfig.BranchName <- $\"{returnValue.Properties[nameof BranchName]}\"\n                                    newConfig.DefaultBranchName <- \"main\"\n                                    newConfig.ObjectStorageProvider <- ObjectStorageProvider.AzureBlobStorage\n                                    updateConfiguration newConfig\n\n                                return result |> renderOutput parseResult\n                            | Error error ->\n                                logToAnsiConsole Colors.Error (Markup.Escape($\"{error}\"))\n                                return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Init subcommand\n    /// Validates that the directory exists. Placed here so it can use InitParameters.\n    let ``Directory must be a valid path`` (parseResult: ParseResult) =\n        if\n            parseResult.CommandResult.Command.Options.Contains(Options.directory)\n            && not\n               <| Directory.Exists(parseResult.GetValue(Options.directory))\n        then\n            Error(GraceError.Create (RepositoryError.getErrorMessage InvalidDirectory) (getCorrelationId parseResult))\n        else\n            Ok parseResult\n\n    let private initHandler (parseResult: ParseResult) (parameters: InitParameters) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let directoryIsValid = parseResult |> ``Directory must be a valid path``\n\n                match directoryIsValid with\n                | Ok _ ->\n                    let validateIncomingParameters = parseResult |> CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                        let isEmptyParameters =\n                            Repository.IsEmptyParameters(\n                                OwnerId = parameters.OwnerId,\n                                OwnerName = parameters.OwnerName,\n                                OrganizationId = parameters.OrganizationId,\n                                OrganizationName = parameters.OrganizationName,\n                                RepositoryId = parameters.RepositoryId,\n                                RepositoryName = parameters.RepositoryName,\n                                CorrelationId = parameters.CorrelationId\n                            )\n\n                        let! repositoryIsEmpty = Repository.IsEmpty isEmptyParameters\n\n                        match repositoryIsEmpty with\n                        | Ok isEmpty ->\n                            if isEmpty.ReturnValue = true then\n                                let repositoryId = RepositoryId.Parse(parameters.RepositoryId)\n\n                                if parseResult |> hasOutput then\n                                    let! graceStatus =\n                                        progress\n                                            .Columns(progressColumns)\n                                            .StartAsync(fun progressContext ->\n                                                task {\n                                                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Reading existing Grace index file.[/]\")\n\n                                                    let t1 =\n                                                        progressContext.AddTask($\"[{Color.DodgerBlue1}]Computing new Grace index file.[/]\", autoStart = false)\n\n                                                    let t2 =\n                                                        progressContext.AddTask($\"[{Color.DodgerBlue1}]Writing new Grace index file.[/]\", autoStart = false)\n\n                                                    let t3 =\n                                                        progressContext.AddTask(\n                                                            $\"[{Color.DodgerBlue1}]Ensure files are in the object cache.[/]\",\n                                                            autoStart = false\n                                                        )\n\n                                                    let t4 =\n                                                        progressContext.AddTask(\n                                                            $\"[{Color.DodgerBlue1}]Ensure object cache index is up-to-date.[/]\",\n                                                            autoStart = false\n                                                        )\n\n                                                    let t5 =\n                                                        progressContext.AddTask(\n                                                            $\"[{Color.DodgerBlue1}]Ensure files are uploaded to object storage.[/]\",\n                                                            autoStart = false\n                                                        )\n\n                                                    let t6 =\n                                                        progressContext.AddTask(\n                                                            $\"[{Color.DodgerBlue1}]Ensure directory versions are uploaded to Grace Server.[/]\",\n                                                            autoStart = false\n                                                        )\n\n                                                    // Read the existing Grace status file.\n                                                    t0.Increment(0.0)\n                                                    let! previousGraceStatus = readGraceStatusFile ()\n                                                    t0.Increment(100.0)\n\n                                                    // Compute the new Grace status file, based on the contents of the working directory.\n                                                    t1.StartTask()\n\n                                                    let! graceStatus = createNewGraceStatusFile previousGraceStatus parseResult\n\n                                                    t1.Value <- 100.0\n\n                                                    // Write the new Grace status file to disk.\n                                                    t2.StartTask()\n                                                    do! writeGraceStatusFile graceStatus\n                                                    t2.Value <- 100.0\n\n                                                    // Ensure all files are in the object cache.\n                                                    t3.StartTask()\n\n                                                    let fileVersions = ConcurrentDictionary<RelativePath, LocalFileVersion>()\n\n                                                    // Loop through the local directory versions, and populate fileVersions with all of the files in the repo.\n                                                    let plr =\n                                                        Parallel.ForEach(\n                                                            graceStatus.Index.Values,\n                                                            Constants.ParallelOptions,\n                                                            (fun ldv ->\n                                                                for fileVersion in ldv.Files do\n                                                                    fileVersions.TryAdd(fileVersion.RelativePath, fileVersion)\n                                                                    |> ignore)\n                                                        )\n\n                                                    let incrementAmount = 100.0 / double fileVersions.Count\n\n                                                    // Loop through the files, and copy them to the object cache if they don't already exist.\n                                                    let plr =\n                                                        Parallel.ForEach(\n                                                            fileVersions,\n                                                            Constants.ParallelOptions,\n                                                            (fun kvp _ ->\n                                                                let fileVersion = kvp.Value\n                                                                let fullObjectPath = fileVersion.FullObjectPath\n\n                                                                if not <| File.Exists(fullObjectPath) then\n                                                                    Directory.CreateDirectory(Path.GetDirectoryName(fullObjectPath))\n                                                                    |> ignore // If the directory already exists, this will do nothing.\n\n                                                                    File.Copy(Path.Combine(Current().RootDirectory, fileVersion.RelativePath), fullObjectPath)\n\n                                                                t3.Increment(incrementAmount))\n                                                        )\n\n                                                    t3.Value <- 100.0\n\n                                                    // Ensure the object cache index is up-to-date.\n                                                    t4.StartTask()\n                                                    do! upsertObjectCache graceStatus.Index.Values\n                                                    t4.Value <- 100.0\n\n                                                    // Ensure all files are uploaded to object storage.\n                                                    t5.StartTask()\n                                                    let incrementAmount = 100.0 / double fileVersions.Count\n\n                                                    match Current().ObjectStorageProvider with\n                                                    | ObjectStorageProvider.Unknown -> ()\n                                                    | AzureBlobStorage ->\n                                                        // Breaking the uploads into chunks allows us to interleave checking to see if files are already uploaded with actually uploading them when they don't.\n                                                        let chunkSize = 32\n                                                        let fileVersionGroups = fileVersions.Chunk(chunkSize)\n                                                        let succeeded = ConcurrentQueue<GraceReturnValue<string>>()\n                                                        let errors = ConcurrentQueue<GraceError>()\n\n                                                        // Loop through the groups of file versions, and upload files that aren't already in object storage.\n                                                        do!\n                                                            Parallel.ForEachAsync(\n                                                                fileVersionGroups,\n                                                                Constants.ParallelOptions,\n                                                                (fun fileVersions ct ->\n                                                                    ValueTask(\n                                                                        task {\n                                                                            let getUploadMetadataForFilesParameters =\n                                                                                GetUploadMetadataForFilesParameters(\n                                                                                    OwnerId = parameters.OwnerId,\n                                                                                    OwnerName = parameters.OwnerName,\n                                                                                    OrganizationId = parameters.OrganizationId,\n                                                                                    OrganizationName = parameters.OrganizationName,\n                                                                                    RepositoryId = parameters.RepositoryId,\n                                                                                    RepositoryName = parameters.RepositoryName,\n                                                                                    CorrelationId = getCorrelationId parseResult,\n                                                                                    FileVersions =\n                                                                                        (fileVersions\n                                                                                         |> Seq.map (fun kvp -> kvp.Value.ToFileVersion)\n                                                                                         |> Seq.toArray)\n                                                                                )\n\n                                                                            let! graceResult =\n                                                                                Storage.GetUploadMetadataForFiles getUploadMetadataForFilesParameters\n\n                                                                            match graceResult with\n                                                                            | Ok graceReturnValue ->\n                                                                                let uploadMetadata = graceReturnValue.ReturnValue\n                                                                                // Increment the counter for the files that we don't have to upload.\n                                                                                t5.Increment(\n                                                                                    incrementAmount\n                                                                                    * double (fileVersions.Count() - uploadMetadata.Count)\n                                                                                )\n\n                                                                                // Index all of the file versions by their SHA256 hash; we'll look up the files to upload with it.\n                                                                                let filesIndexedBySha256Hash =\n                                                                                    Dictionary<Sha256Hash, LocalFileVersion>(\n                                                                                        fileVersions.Select (fun kvp ->\n                                                                                            KeyValuePair(kvp.Value.Sha256Hash, kvp.Value))\n                                                                                    )\n\n                                                                                // Upload the files in this chunk to object storage.\n                                                                                do!\n                                                                                    Parallel.ForEachAsync(\n                                                                                        uploadMetadata,\n                                                                                        Constants.ParallelOptions,\n                                                                                        (fun upload ct ->\n                                                                                            ValueTask(\n                                                                                                task {\n                                                                                                    let fileVersion =\n                                                                                                        filesIndexedBySha256Hash[upload.Sha256Hash]\n                                                                                                            .ToFileVersion\n\n                                                                                                    let! result =\n                                                                                                        Storage.SaveFileToObjectStorage\n                                                                                                            repositoryId\n                                                                                                            fileVersion\n                                                                                                            (upload.BlobUriWithSasToken)\n                                                                                                            (getCorrelationId parseResult)\n\n                                                                                                    // Increment the counter for each file that we do upload.\n                                                                                                    t5.Increment(incrementAmount)\n\n                                                                                                    match result with\n                                                                                                    | Ok result -> succeeded.Enqueue(result)\n                                                                                                    | Error error -> errors.Enqueue(error)\n                                                                                                }\n                                                                                            ))\n                                                                                    )\n\n                                                                            | Error error -> AnsiConsole.Write((new Panel($\"{error}\")).BorderColor(Color.Red3))\n                                                                        }\n                                                                    ))\n                                                            )\n\n                                                        // Print out any errors that occurred.\n                                                        if errors |> Seq.isEmpty then\n                                                            ()\n                                                        else\n                                                            AnsiConsole.MarkupLine($\"{errors.Count} errors occurred.\")\n\n                                                            let mutable error = GraceError.Create String.Empty String.Empty\n\n                                                            while not <| errors.IsEmpty do\n                                                                if errors.TryDequeue(&error) then\n                                                                    AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error.EscapeMarkup()}[/]\")\n\n                                                            ()\n                                                    | AWSS3 -> ()\n                                                    | GoogleCloudStorage -> ()\n\n                                                    t5.Value <- 100.0\n\n                                                    // Ensure all directory versions are uploaded to Grace Server.\n                                                    t6.StartTask()\n                                                    let chunkSize = 16\n                                                    let succeeded = ConcurrentQueue<GraceReturnValue<string>>()\n                                                    let errors = ConcurrentQueue<GraceError>()\n                                                    let incrementAmount = 100.0 / double graceStatus.Index.Count\n\n                                                    // We'll segment the uploads by the number of segments in the path,\n                                                    //   so we process the deepest paths first, and the new children exist before the parent is created.\n                                                    //   Within each segment group, we'll parallelize the processing for performance.\n                                                    let segmentGroups =\n                                                        graceStatus\n                                                            .Index\n                                                            .Values\n                                                            .GroupBy(fun dv -> countSegments dv.RelativePath)\n                                                            .OrderByDescending(fun group -> group.Key)\n\n                                                    for group in segmentGroups do\n                                                        let directoryVersionGroups = group.Chunk(chunkSize)\n\n                                                        do!\n                                                            Parallel.ForEachAsync(\n                                                                directoryVersionGroups,\n                                                                Constants.ParallelOptions,\n                                                                (fun directoryVersionGroup ct ->\n                                                                    ValueTask(\n                                                                        task {\n                                                                            let saveParameters = SaveDirectoryVersionsParameters()\n                                                                            saveParameters.OwnerId <- parameters.OwnerId\n                                                                            saveParameters.OwnerName <- parameters.OwnerName\n                                                                            saveParameters.OrganizationId <- parameters.OrganizationId\n                                                                            saveParameters.OrganizationName <- parameters.OrganizationName\n                                                                            saveParameters.RepositoryId <- parameters.RepositoryId\n                                                                            saveParameters.RepositoryName <- parameters.RepositoryName\n                                                                            saveParameters.CorrelationId <- getCorrelationId parseResult\n\n                                                                            saveParameters.DirectoryVersions <-\n                                                                                directoryVersionGroup\n                                                                                    .Select(fun dv -> dv.ToDirectoryVersion)\n                                                                                    .ToList()\n\n                                                                            let! sdvResult = DirectoryVersion.SaveDirectoryVersions saveParameters\n\n                                                                            match sdvResult with\n                                                                            | Ok result -> succeeded.Enqueue(result)\n                                                                            | Error error -> errors.Enqueue(error)\n\n                                                                            t6.Increment(\n                                                                                incrementAmount\n                                                                                * double directoryVersionGroup.Length\n                                                                            )\n                                                                        }\n                                                                    ))\n                                                            )\n\n                                                    t6.Value <- 100.0\n\n                                                    AnsiConsole.MarkupLine($\"[{Colors.Important}]succeeded: {succeeded.Count}; errors: {errors.Count}.[/]\")\n\n                                                    let mutable error = GraceError.Create String.Empty String.Empty\n\n                                                    while not <| errors.IsEmpty do\n                                                        errors.TryDequeue(&error) |> ignore\n\n                                                        if error.Error.Contains(\"TRetval\") then logToConsole $\"********* {error.Error}\"\n\n                                                        AnsiConsole.MarkupLine($\"[{Colors.Error}]{error.Error.EscapeMarkup()}[/]\")\n\n                                                    return graceStatus\n\n                                                })\n\n                                    let fileCount =\n                                        graceStatus\n                                            .Index\n                                            .Values\n                                            .Select(fun directoryVersion -> directoryVersion.Files.Count)\n                                            .Sum()\n\n                                    let totalFileSize = graceStatus.Index.Values.Sum(fun directoryVersion -> directoryVersion.Files.Sum(fun f -> int64 f.Size))\n\n                                    let rootDirectoryVersion = graceStatus.Index.Values.First(fun d -> d.RelativePath = Constants.RootDirectoryPath)\n\n                                    AnsiConsole.MarkupLine($\"[{Colors.Highlighted}]Number of directories scanned: {graceStatus.Index.Count}.[/]\")\n\n                                    AnsiConsole.MarkupLine(\n                                        $\"[{Colors.Highlighted}]Number of files scanned: {fileCount}; total file size: {totalFileSize:N0}.[/]\"\n                                    )\n\n                                    AnsiConsole.MarkupLine $\"[{Colors.Highlighted}]Root SHA-256 hash: {rootDirectoryVersion.Sha256Hash.Substring(0, 8)}[/]\"\n\n                                    return Ok(GraceReturnValue.Create \"Initialized repository.\" (parseResult |> getCorrelationId))\n                                else\n                                    // Do the whole thing with no output\n                                    return Ok(GraceReturnValue.Create \"Initialized repository.\" (parseResult |> getCorrelationId))\n                            else\n                                return\n                                    Error(GraceError.Create (RepositoryError.getErrorMessage RepositoryIsAlreadyInitialized) (parseResult |> getCorrelationId))\n                        | Error error -> return Error error\n                    // Take functionality from grace maint update... most of it is already there.\n                    // We need to double-check that we have the correct owner/organization/repository because we're\n                    //   going to be uploading files to object storage placed in containers named after the owner/organization/repository.\n                    // Test on small, medium, and large repositories.\n                    // Test on repositories with multiple branches - should fail.\n                    // Test on repositories with only initial branch and no references - should succeed.\n                    // Test on repositories with only initial branch and references - should fail.\n                    // Test on repositories with multiple branches and references - should fail.\n                    | Error error -> return Error error\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    type Init() =\n        inherit AsynchronousCommandLineAction()\n\n        override this.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                        // Build the InitParameters object from the parsed values so we can reuse existing handler logic.\n                        let initParameters = InitParameters()\n                        initParameters.OwnerId <- graceIds.OwnerIdString\n                        initParameters.OwnerName <- graceIds.OwnerName\n                        initParameters.OrganizationId <- graceIds.OrganizationIdString\n                        initParameters.OrganizationName <- graceIds.OrganizationName\n                        initParameters.RepositoryId <- graceIds.RepositoryIdString\n                        initParameters.RepositoryName <- graceIds.RepositoryName\n                        initParameters.GraceConfig <- parseResult.GetValue(Options.graceConfig)\n                        initParameters.CorrelationId <- getCorrelationId parseResult\n\n                        let! result = initHandler parseResult initParameters\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n\n    /// Repository.Get subcommand definition\n    type Get() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Parameters.Repository.GetRepositoryParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Repository.Get(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.Get(parameters)\n\n                        match result with\n                        | Ok graceReturnValue ->\n                            let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                            AnsiConsole.Write(jsonText)\n                            AnsiConsole.WriteLine()\n                            return Ok graceReturnValue |> renderOutput parseResult\n                        | Error graceError -> return Error graceError |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.GetBranches subcommand definition\n    type GetBranches() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.GetBranchesParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                IncludeDeleted = parseResult.GetValue(Options.includeDeleted),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.GetBranches(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.GetBranches(parameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            let rendered = Ok returnValue |> renderOutput parseResult\n\n                            if parseResult |> hasOutput then\n                                let table = Table(Border = TableBorder.DoubleEdge)\n\n                                table.AddColumns(\n                                    [|\n                                        TableColumn($\"[{Colors.Important}]Branch name[/]\")\n                                        TableColumn($\"[{Colors.Important}]Branch Id[/]\")\n                                        TableColumn($\"[{Colors.Important}]SHA-256 hash[/]\")\n                                        TableColumn($\"[{Colors.Important}]Based on latest promotion[/]\")\n                                        TableColumn($\"[{Colors.Important}]Parent branch[/]\")\n                                        TableColumn($\"[{Colors.Important}]When[/]\", Alignment = Justify.Right)\n                                        TableColumn($\"[{Colors.Important}]Updated at[/]\")\n                                    |]\n                                )\n                                |> ignore\n\n                                let allBranches = returnValue.ReturnValue\n\n                                // Get the parent branch names and latest promotions for all branches\n                                let parents =\n                                    allBranches.Select (fun branch ->\n                                        {|\n                                            BranchId = branch.BranchId\n                                            BranchName =\n                                                if branch.ParentBranchId = Constants.DefaultParentBranchId then\n                                                    \"root\"\n                                                else\n                                                    allBranches\n                                                        .Where(fun br -> br.BranchId = branch.ParentBranchId)\n                                                        .Select(fun br -> br.BranchName)\n                                                        .First()\n                                            LatestPromotion =\n                                                if branch.ParentBranchId = Constants.DefaultParentBranchId then\n                                                    branch.LatestPromotion\n                                                else\n                                                    allBranches\n                                                        .Where(fun br -> br.BranchId = branch.ParentBranchId)\n                                                        .Select(fun br -> br.LatestPromotion)\n                                                        .First()\n                                        |})\n\n                                let branchesWithParentNames =\n                                    allBranches\n                                        .Join(\n                                            parents,\n                                            (fun branch -> branch.BranchId),\n                                            (fun parent -> parent.BranchId),\n                                            (fun branch parent ->\n                                                {|\n                                                    BranchId = branch.BranchId\n                                                    BranchName = branch.BranchName\n                                                    Sha256Hash = branch.LatestReference.Sha256Hash\n                                                    UpdatedAt = branch.UpdatedAt\n                                                    Ago = ago branch.CreatedAt\n                                                    ParentBranchName = parent.BranchName\n                                                    BasedOnLatestPromotion = (branch.BasedOn.ReferenceId = parent.LatestPromotion.ReferenceId)\n                                                |})\n                                        )\n                                        .OrderBy(fun branch -> branch.UpdatedAt)\n\n                                for br in branchesWithParentNames do\n                                    let updatedAt =\n                                        match br.UpdatedAt with\n                                        | Some t -> instantToLocalTime (t)\n                                        | None -> String.Empty\n\n                                    table.AddRow(\n                                        br.BranchName,\n                                        $\"[{Colors.Deemphasized}]{br.BranchId}[/]\",\n                                        br.Sha256Hash |> getShortSha256Hash,\n                                        (if br.BasedOnLatestPromotion then\n                                             $\"[{Colors.Added}]Yes[/]\"\n                                         else\n                                             $\"[{Colors.Important}]No[/]\"),\n                                        br.ParentBranchName,\n                                        br.Ago,\n                                        $\"[{Colors.Deemphasized}]{updatedAt}[/]\"\n                                    )\n                                    |> ignore\n\n                                AnsiConsole.Write(table)\n\n                            return rendered\n                        | Error _ -> return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetVisibility subcommand definition\n    type SetVisibility() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let visibilityParameters =\n                            Repository.SetRepositoryVisibilityParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                Visibility =\n                                    (parseResult.GetValue(Options.visibility))\n                                        .ToString(),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetVisibility(visibilityParameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetVisibility(visibilityParameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetStatus subcommand definition\n    type SetStatus() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetRepositoryStatusParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                Status = parseResult.GetValue(Options.status),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetStatus(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetStatus(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetRecordSaves subcommand definition\n    type SetRecordSaves() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.RecordSavesParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                RecordSaves = parseResult.GetValue(Options.recordSaves),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetRecordSaves(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetRecordSaves(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetSaveDays subcommand definition\n    type SetSaveDays() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetSaveDaysParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                SaveDays = parseResult.GetValue(Options.saveDays)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetSaveDays(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetSaveDays(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetCheckpointDays subcommand definition\n    type SetCheckpointDays() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetCheckpointDaysParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                CheckpointDays = parseResult.GetValue(Options.checkpointDays)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetCheckpointDays(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetCheckpointDays(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetDiffCacheDays subcommand definition\n    type SetDiffCacheDays() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetDiffCacheDaysParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                DiffCacheDays = parseResult.GetValue(Options.diffCacheDays)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetDiffCacheDays(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetDiffCacheDays(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetDirectoryVersionCacheDays subcommand definition\n    type SetDirectoryVersionCacheDays() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetDirectoryVersionCacheDaysParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                DirectoryVersionCacheDays = parseResult.GetValue(Options.directoryVersionCacheDays)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetDirectoryVersionCacheDays(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetDirectoryVersionCacheDays(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetLogicalDeleteDays subcommand definition\n    type SetLogicalDeleteDays() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetLogicalDeleteDaysParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                LogicalDeleteDays = parseResult.GetValue(Options.logicalDeleteDays)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                            let! response = Repository.SetLogicalDeleteDays(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetLogicalDeleteDays(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    // Enable promotion type subcommands\n    type EnablePromotionTypeCommand = EnablePromotionTypeParameters -> Task<GraceResult<string>>\n\n    type EnablePromotionParameters() =\n        member val public Enabled = false with get, set\n\n    let private enablePromotionTypeHandler\n        (parseResult: ParseResult)\n        (parameters: EnablePromotionParameters)\n        (command: EnablePromotionTypeCommand)\n        (promotionType: PromotionType)\n        =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n\n                let validateIncomingParameters = CommonValidations parseResult\n\n                match validateIncomingParameters with\n                | Ok _ ->\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let enablePromotionTypeParameters =\n                        EnablePromotionTypeParameters(\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId,\n                            Enabled = parameters.Enabled\n                        )\n\n                    if parseResult |> hasOutput then\n                        return!\n                            progress\n                                .Columns(progressColumns)\n                                .StartAsync(fun progressContext ->\n                                    task {\n                                        let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                        let! result = command enablePromotionTypeParameters\n                                        t0.Increment(100.0)\n                                        return result\n                                    })\n                    else\n                        return! command enablePromotionTypeParameters\n                | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId))\n        }\n\n    // Set-DefaultServerApiVersion subcommand\n    /// Repository.SetDefaultServerApiVersion subcommand definition\n    type SetDefaultServerApiVersion() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetDefaultServerApiVersionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                DefaultServerApiVersion = parseResult.GetValue(Options.defaultServerApiVersion)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetDefaultServerApiVersion(parameters)\n\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetDefaultServerApiVersion(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetName subcommand definition\n    type SetName() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetRepositoryNameParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                NewName = parseResult.GetValue(Options.newName)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetName(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetName(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetDescription subcommand definition\n    type SetDescription() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetRepositoryDescriptionParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                Description = parseResult.GetValue(Options.description)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.SetDescription(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetDescription(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetConflictResolutionPolicy subcommand definition\n    type SetConflictResolutionPolicy() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetConflictResolutionPolicyParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                ConflictResolutionPolicy = parseResult.GetValue(Options.conflictResolutionPolicy),\n                                ConfidenceThreshold = parseResult.GetValue(Options.confidenceThreshold)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Grace.SDK.Repository.SetConflictResolutionPolicy(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Grace.SDK.Repository.SetConflictResolutionPolicy(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.Delete subcommand definition\n    type Delete() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.DeleteRepositoryParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult,\n                                Force = parseResult.GetValue(Options.force),\n                                DeleteReason = parseResult.GetValue(Options.deleteReason)\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.Delete(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.Delete(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.Undelete subcommand definition\n    type Undelete() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.UndeleteRepositoryParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n\n                                            let! response = Repository.Undelete(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.Undelete(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetAnonymousAccess subcommand definition\n    type SetAnonymousAccess() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetAnonymousAccessParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                AnonymousAccess = parseResult.GetValue(Options.anonymousAccess),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Setting anonymous access.[/]\")\n                                            let! response = Repository.SetAnonymousAccess(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetAnonymousAccess(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Repository.SetAllowsLargeFiles subcommand definition\n    type SetAllowsLargeFiles() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Tasks.Task<int> =\n            task {\n                try\n                    if parseResult |> verbose then printParseResult parseResult\n                    let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                    let validateIncomingParameters =\n                        parseResult\n                        |> Grace.CLI.Common.Validations.CommonValidations\n\n                    match validateIncomingParameters with\n                    | Ok _ ->\n                        let parameters =\n                            Repository.SetAllowsLargeFilesParameters(\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                AllowsLargeFiles = parseResult.GetValue(Options.allowsLargeFiles),\n                                CorrelationId = getCorrelationId parseResult\n                            )\n\n                        let! result =\n                            if parseResult |> hasOutput then\n                                progress\n                                    .Columns(progressColumns)\n                                    .StartAsync(fun progressContext ->\n                                        task {\n                                            let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Setting allows large files.[/]\")\n                                            let! response = Repository.SetAllowsLargeFiles(parameters)\n                                            t0.Increment(100.0)\n                                            return response\n                                        })\n                            else\n                                Repository.SetAllowsLargeFiles(parameters)\n\n                        return result |> renderOutput parseResult\n                    | Error error -> return Error error |> renderOutput parseResult\n\n                with\n                | ex ->\n                    return\n                        renderOutput\n                            parseResult\n                            (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n            }\n\n    /// Builds the Repository subcommand.\n    let Build =\n        let addCommonOptionsExceptForRepositoryInfo (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryId\n\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.repositoryName\n            |> addCommonOptionsExceptForRepositoryInfo\n\n        // Create main command and aliases, if any.\n        let repositoryCommand = new Command(\"repository\", Description = \"Creates, changes, and deletes repository-level information.\")\n\n        repositoryCommand.Aliases.Add(\"repo\")\n\n        // Add subcommands.\n        let repositoryCreateCommand =\n            new Command(\"create\", Description = \"Creates a new repository.\")\n            |> addOption Options.requiredRepositoryName\n            |> addCommonOptionsExceptForRepositoryInfo\n            |> addOption Options.doNotSwitch\n\n        repositoryCreateCommand.Action <- new Create()\n        repositoryCommand.Subcommands.Add(repositoryCreateCommand)\n\n        let repositoryGetCommand =\n            new Command(\"get\", Description = \"Gets information about a repository.\")\n            |> addCommonOptions\n\n        repositoryGetCommand.Action <- new Get()\n        repositoryCommand.Subcommands.Add(repositoryGetCommand)\n\n        let repositoryInitCommand =\n            new Command(\"init\", Description = \"Initializes a new repository with the contents of a directory.\")\n            |> addOption Options.directory\n            |> addOption Options.graceConfig\n            |> addCommonOptions\n\n        repositoryInitCommand.Action <- new Init()\n        repositoryCommand.Subcommands.Add(repositoryInitCommand)\n\n        //let repositoryDownloadCommand = new Command(\"download\", Description = \"Downloads the current version of the repository.\") |> addOption Options.requiredRepositoryName |> addOption Options.graceConfig |> addCommonOptionsExceptForRepositoryInfo\n        //repositoryInitCommand.Action <- Init\n        //repositoryCommand.Subcommands.Add(repositoryInitCommand)\n\n        let getBranchesCommand =\n            new Command(\"get-branches\", Description = \"Gets a list of branches in the repository.\")\n            |> addOption Options.includeDeleted\n            |> addCommonOptions\n\n        getBranchesCommand.Action <- new GetBranches()\n        repositoryCommand.Subcommands.Add(getBranchesCommand)\n\n        let setVisibilityCommand =\n            new Command(\"set-visibility\", Description = \"Sets the visibility of the repository.\")\n            |> addOption Options.visibility\n            |> addCommonOptions\n\n        setVisibilityCommand.Action <- new SetVisibility()\n        repositoryCommand.Subcommands.Add(setVisibilityCommand)\n\n        let setStatusCommand =\n            new Command(\"set-status\", Description = \"Sets the status of the repository.\")\n            |> addOption Options.status\n            |> addCommonOptions\n\n        setStatusCommand.Action <- new SetStatus()\n        repositoryCommand.Subcommands.Add(setStatusCommand)\n\n        let setAnonymousAccessCommand =\n            new Command(\"set-anonymous-access\", Description = \"Sets the anonymous access status of the repository.\")\n            |> addOption Options.anonymousAccess\n            |> addCommonOptions\n\n        setAnonymousAccessCommand.Action <- new SetAnonymousAccess()\n        repositoryCommand.Subcommands.Add(setAnonymousAccessCommand)\n\n        let setAllowsLargeFilesCommand =\n            new Command(\"set-allows-large-files\", Description = \"Sets the large files status of the repository.\")\n            |> addOption Options.allowsLargeFiles\n            |> addCommonOptions\n\n        setAllowsLargeFilesCommand.Action <- new SetAllowsLargeFiles()\n        repositoryCommand.Subcommands.Add(setAllowsLargeFilesCommand)\n\n        let setRecordSavesCommand =\n            new Command(\"set-record-saves\", Description = \"Sets whether the repository defaults to recording every save.\")\n            |> addOption Options.recordSaves\n            |> addCommonOptions\n\n        setRecordSavesCommand.Action <- new SetRecordSaves()\n        repositoryCommand.Subcommands.Add(setRecordSavesCommand)\n\n        let setDefaultServerApiVersionCommand =\n            new Command(\n                \"set-default-server-api-version\",\n                Description = \"Sets the default server API version for clients to use when accessing this repository.\"\n            )\n            |> addOption Options.defaultServerApiVersion\n            |> addCommonOptions\n\n        setDefaultServerApiVersionCommand.Action <- new SetDefaultServerApiVersion()\n        repositoryCommand.Subcommands.Add(setDefaultServerApiVersionCommand)\n\n        let setSaveDaysCommand =\n            new Command(\"set-save-days\", Description = \"Sets the number of days to keep saves in the repository.\")\n            |> addOption Options.saveDays\n            |> addCommonOptions\n\n        setSaveDaysCommand.Action <- new SetSaveDays()\n        repositoryCommand.Subcommands.Add(setSaveDaysCommand)\n\n        let setCheckpointDaysCommand =\n            new Command(\"set-checkpoint-days\", Description = \"Sets the number of days to keep checkpoints in the repository.\")\n            |> addOption Options.checkpointDays\n            |> addCommonOptions\n\n        setCheckpointDaysCommand.Action <- new SetCheckpointDays()\n        repositoryCommand.Subcommands.Add(setCheckpointDaysCommand)\n\n        let setDiffCacheDaysCommand =\n            new Command(\"set-diff-cache-days\", Description = \"Sets the number of days to keep diff results cached in the repository.\")\n            |> addOption Options.diffCacheDays\n            |> addCommonOptions\n\n        setDiffCacheDaysCommand.Action <- new SetDiffCacheDays()\n        repositoryCommand.Subcommands.Add(setDiffCacheDaysCommand)\n\n        let setDirectoryVersionCacheDaysCommand =\n            new Command(\n                \"set-directory-version-cache-days\",\n                Description = \"Sets how long to keep recursive directory version contents cached in the repository.\"\n            )\n            |> addOption Options.directoryVersionCacheDays\n            |> addCommonOptions\n\n        setDirectoryVersionCacheDaysCommand.Action <- new SetDirectoryVersionCacheDays()\n        repositoryCommand.Subcommands.Add(setDirectoryVersionCacheDaysCommand)\n\n        let setLogicalDeleteDaysCommand =\n            new Command(\n                \"set-logical-delete-days\",\n                Description = \"Sets the number of days to keep deleted branches in the repository before permanently deleting them.\"\n            )\n            |> addOption Options.logicalDeleteDays\n            |> addCommonOptions\n\n        setLogicalDeleteDaysCommand.Action <- new SetLogicalDeleteDays()\n        repositoryCommand.Subcommands.Add(setLogicalDeleteDaysCommand)\n\n        let setNameCommand =\n            new Command(\"set-name\", Description = \"Sets the name of the repository.\")\n            |> addOption Options.newName\n            |> addCommonOptions\n\n        setNameCommand.Action <- new SetName()\n        repositoryCommand.Subcommands.Add(setNameCommand)\n\n        let setDescriptionCommand =\n            new Command(\"set-description\", Description = \"Sets the description of the repository.\")\n            |> addOption Options.description\n            |> addCommonOptions\n\n        setDescriptionCommand.Action <- new SetDescription()\n        repositoryCommand.Subcommands.Add(setDescriptionCommand)\n\n        let setConflictResolutionPolicyCommand =\n            new Command(\"set-conflict-resolution-policy\", Description = \"Sets the conflict resolution policy for the repository.\")\n            |> addOption Options.conflictResolutionPolicy\n            |> addOption Options.confidenceThreshold\n            |> addCommonOptions\n\n        setConflictResolutionPolicyCommand.Action <- new SetConflictResolutionPolicy()\n        repositoryCommand.Subcommands.Add(setConflictResolutionPolicyCommand)\n\n        let deleteCommand =\n            new Command(\"delete\", Description = \"Deletes a repository.\")\n            |> addOption Options.deleteReason\n            |> addOption Options.force\n            |> addCommonOptions\n\n        deleteCommand.Action <- new Delete()\n        repositoryCommand.Subcommands.Add(deleteCommand)\n\n        let undeleteCommand =\n            new Command(\"undelete\", Description = \"Undeletes the repository.\")\n            |> addCommonOptions\n\n        undeleteCommand.Action <- new Undelete()\n        repositoryCommand.Subcommands.Add(undeleteCommand)\n\n        repositoryCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Review.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Policy\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen Spectre.Console\nopen System\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.IO\nopen System.Text\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule ReviewCommand =\n    module private Options =\n        let promotionSetId =\n            new Option<string>(\n                \"--promotion-set\",\n                [| \"--promotion-set-id\" |],\n                Required = false,\n                Description = \"The promotion set ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let referenceId =\n            new Option<string>(\n                OptionName.ReferenceId,\n                Required = true,\n                Description = \"The reference ID <Guid> to mark reviewed.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let policySnapshotId =\n            new Option<string>(\n                \"--policy-snapshot-id\",\n                Required = false,\n                Description = \"Policy snapshot ID for this checkpoint.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let findingId = new Option<string>(\"--finding-id\", Required = true, Description = \"The finding ID <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n        let approve =\n            new Option<bool>(\n                \"--approve\",\n                Required = false,\n                Description = \"Approve the finding.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let requestChanges =\n            new Option<bool>(\n                \"--request-changes\",\n                Required = false,\n                Description = \"Request changes for the finding.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let note = new Option<string>(\"--note\", Required = false, Description = \"Optional note for the resolution.\", Arity = ArgumentArity.ExactlyOne)\n\n        let chapterId =\n            new Option<string>(\"--chapter\", Required = false, Description = \"Chapter ID <Sha256Hash> for targeted deepening.\", Arity = ArgumentArity.ExactlyOne)\n\n        let candidateId =\n            new Option<string>(\n                \"--candidate\",\n                [| \"--candidate-id\" |],\n                Required = true,\n                Description = \"The candidate ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let reportFormat = new Option<string>(\"--format\", Required = true, Description = \"Export format: markdown or json.\", Arity = ArgumentArity.ExactlyOne)\n\n        let outputFile =\n            new Option<string>(\n                \"--output-file\",\n                [| \"-f\" |],\n                Required = true,\n                Description = \"Write exported report content to this file path.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let targetBranch =\n            new Option<string>(\n                \"--target-branch\",\n                Required = false,\n                Description = \"Target branch ID or name for review inbox.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The organization's name. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                Required = false,\n                Description = \"The repository's name. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    let private tryParseGuid (value: string) (error: ReviewError) (parseResult: ParseResult) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value)\n           || Guid.TryParse(value, &parsed) = false\n           || parsed = Guid.Empty then\n            Error(GraceError.Create (ReviewError.getErrorMessage error) (getCorrelationId parseResult))\n        else\n            Ok parsed\n\n    let internal resolvePolicySnapshotIdWith\n        (getPromotionSet: Parameters.PromotionSet.GetPromotionSetParameters -> Task<GraceResult<Grace.Types.PromotionSet.PromotionSetDto>>)\n        (getPolicy: Parameters.Policy.GetPolicyParameters -> Task<GraceResult<PolicySnapshot option>>)\n        (parseResult: ParseResult)\n        (graceIds: GraceIds)\n        (promotionSetId: Guid)\n        =\n        task {\n            let rawPolicySnapshotId =\n                parseResult.GetValue(Options.policySnapshotId)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            if not (String.IsNullOrWhiteSpace rawPolicySnapshotId) then\n                return Ok rawPolicySnapshotId\n            else\n                let promotionSetParameters =\n                    Parameters.PromotionSet.GetPromotionSetParameters(\n                        PromotionSetId = promotionSetId.ToString(),\n                        OwnerId = graceIds.OwnerIdString,\n                        OwnerName = graceIds.OwnerName,\n                        OrganizationId = graceIds.OrganizationIdString,\n                        OrganizationName = graceIds.OrganizationName,\n                        RepositoryId = graceIds.RepositoryIdString,\n                        RepositoryName = graceIds.RepositoryName,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                match! getPromotionSet promotionSetParameters with\n                | Error error -> return Error error\n                | Ok promotionSetReturnValue ->\n                    let promotionSet = promotionSetReturnValue.ReturnValue\n\n                    let policyParameters =\n                        Parameters.Policy.GetPolicyParameters(\n                            TargetBranchId = promotionSet.TargetBranchId.ToString(),\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    match! getPolicy policyParameters with\n                    | Error error -> return Error error\n                    | Ok policyReturnValue ->\n                        match policyReturnValue.ReturnValue with\n                        | Some snapshot when not (String.IsNullOrWhiteSpace snapshot.PolicySnapshotId) -> return Ok snapshot.PolicySnapshotId\n                        | _ -> return Error(GraceError.Create (ReviewError.getErrorMessage ReviewError.InvalidPolicySnapshotId) (getCorrelationId parseResult))\n        }\n\n    let private resolvePolicySnapshotId (parseResult: ParseResult) (graceIds: GraceIds) (promotionSetId: Guid) =\n        resolvePolicySnapshotIdWith PromotionSet.Get Policy.GetCurrent parseResult graceIds promotionSetId\n\n    type private ReportExportFormat =\n        | Markdown\n        | Json\n\n    let private parseReportExportFormat (rawValue: string) (parseResult: ParseResult) =\n        match rawValue.Trim().ToLowerInvariant() with\n        | \"markdown\" -> Ok Markdown\n        | \"json\" -> Ok Json\n        | _ -> Error(GraceError.Create \"Format must be either 'markdown' or 'json'.\" (getCorrelationId parseResult))\n\n    let private resolveCandidateId (parseResult: ParseResult) =\n        let candidateIdRaw =\n            parseResult.GetValue(Options.candidateId)\n            |> Option.ofObj\n            |> Option.defaultValue String.Empty\n\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(candidateIdRaw)\n           || not (Guid.TryParse(candidateIdRaw, &parsed))\n           || parsed = Guid.Empty then\n            Error(GraceError.Create \"CandidateId must be a valid non-empty Guid.\" (getCorrelationId parseResult))\n        else\n            Ok(parsed.ToString())\n\n    let private buildCandidateProjectionParameters (graceIds: GraceIds) (candidateId: string) =\n        Parameters.Review.CandidateProjectionParameters(\n            CandidateId = candidateId,\n            OwnerId = graceIds.OwnerIdString,\n            OwnerName = graceIds.OwnerName,\n            OrganizationId = graceIds.OrganizationIdString,\n            OrganizationName = graceIds.OrganizationName,\n            RepositoryId = graceIds.RepositoryIdString,\n            RepositoryName = graceIds.RepositoryName,\n            CorrelationId = graceIds.CorrelationId\n        )\n\n    let internal normalizeReviewReportForOutput (report: ReviewReportResult) =\n        let normalized = ReviewReportResult()\n        normalized.ReviewReportSchemaVersion <- report.ReviewReportSchemaVersion\n        normalized.SectionOrder <- report.SectionOrder\n\n        let sectionOrderRank =\n            normalized.SectionOrder\n            |> List.mapi (fun index section -> section, index)\n            |> dict\n\n        let normalizedSections =\n            report.Sections\n            |> List.map (fun section ->\n                let normalizedSection = ReviewReportSection()\n                normalizedSection.Section <- section.Section\n                normalizedSection.Title <- section.Title\n                normalizedSection.SourceState <- section.SourceState\n\n                normalizedSection.SourceStates <-\n                    section.SourceStates\n                    |> List.sortBy (fun sourceState -> sourceState.Section, sourceState.SourceState, sourceState.Detail)\n\n                normalizedSection.Entries <-\n                    section.Entries\n                    |> List.sortBy (fun entry -> entry.Key)\n                    |> List.map (fun entry ->\n                        let normalizedEntry = ReviewReportEntry()\n                        normalizedEntry.Key <- entry.Key\n                        normalizedEntry.Values <- entry.Values |> List.sort\n                        normalizedEntry)\n\n                normalizedSection.Diagnostics <- section.Diagnostics |> List.sort\n                normalizedSection)\n            |> List.sortBy (fun section ->\n                (if sectionOrderRank.ContainsKey(section.Section) then\n                     sectionOrderRank[section.Section]\n                 else\n                     Int32.MaxValue),\n                section.Section)\n\n        normalized.Sections <- normalizedSections\n        normalized\n\n    let internal renderReviewReportMarkdown (report: ReviewReportResult) =\n        let normalized = normalizeReviewReportForOutput report\n        let markdown = StringBuilder()\n\n        markdown.AppendLine($\"# Review Report (schema {normalized.ReviewReportSchemaVersion})\")\n        |> ignore\n\n        for section in normalized.Sections do\n            markdown.AppendLine() |> ignore\n\n            markdown.AppendLine($\"## {section.Title}\")\n            |> ignore\n\n            markdown.AppendLine($\"- Section: {section.Section}\")\n            |> ignore\n\n            markdown.AppendLine($\"- SourceState: {section.SourceState}\")\n            |> ignore\n\n            for entry in section.Entries do\n                if entry.Values.IsEmpty then\n                    markdown.AppendLine($\"- {entry.Key}: NotAvailable\")\n                    |> ignore\n                elif entry.Values.Length = 1 then\n                    markdown.AppendLine($\"- {entry.Key}: {entry.Values[0]}\")\n                    |> ignore\n                else\n                    markdown.AppendLine($\"- {entry.Key}:\") |> ignore\n\n                    for value in entry.Values do\n                        markdown.AppendLine($\"  - {value}\") |> ignore\n\n            if not section.Diagnostics.IsEmpty then\n                markdown.AppendLine(\"- Diagnostics:\") |> ignore\n\n                for diagnostic in section.Diagnostics do\n                    markdown.AppendLine($\"  - {diagnostic}\") |> ignore\n\n            if not section.SourceStates.IsEmpty then\n                markdown.AppendLine(\"- SourceStates:\") |> ignore\n\n                for sourceState in section.SourceStates do\n                    markdown.AppendLine($\"  - {sourceState.Section}: {sourceState.SourceState} ({sourceState.Detail})\")\n                    |> ignore\n\n        markdown.ToString().TrimEnd()\n\n    let internal serializeReviewReportJson (report: ReviewReportResult) =\n        let normalized = normalizeReviewReportForOutput report\n        serialize normalized\n\n    let private writeNotesSummary (parseResult: ParseResult) (notes: ReviewNotes) =\n        if\n            not (parseResult |> json)\n            && not (parseResult |> silent)\n        then\n            AnsiConsole.MarkupLine($\"[bold]Review Notes[/] {Markup.Escape(notes.ReviewNotesId.ToString())}\")\n\n            if not (String.IsNullOrWhiteSpace notes.Summary) then\n                AnsiConsole.MarkupLine($\"[bold]Summary:[/] {Markup.Escape(notes.Summary)}\")\n\n            AnsiConsole.MarkupLine($\"[bold]Chapters:[/] {notes.Chapters.Length}  [bold]Findings:[/] {notes.Findings.Length}\")\n\n    let private inboxHandler (parseResult: ParseResult) =\n        task {\n            let graceError = GraceError.Create \"Review inbox is not implemented yet.\" (getCorrelationId parseResult)\n\n            return Error graceError\n        }\n\n    type Inbox() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = inboxHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private openHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let promotionSetIdRaw =\n                    parseResult.GetValue(Options.promotionSetId)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                if String.IsNullOrWhiteSpace promotionSetIdRaw then\n                    return Error(GraceError.Create (ReviewError.getErrorMessage ReviewError.InvalidPromotionSetId) (getCorrelationId parseResult))\n                else\n                    match tryParseGuid promotionSetIdRaw ReviewError.InvalidPromotionSetId parseResult with\n                    | Error error -> return Error error\n                    | Ok promotionSetId ->\n                        let parameters =\n                            Parameters.Review.GetReviewNotesParameters(\n                                PromotionSetId = promotionSetId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        let! result = Review.GetNotes(parameters)\n\n                        match result with\n                        | Ok returnValue ->\n                            match returnValue.ReturnValue with\n                            | Some notes -> writeNotesSummary parseResult notes\n                            | None ->\n                                if\n                                    not (parseResult |> json)\n                                    && not (parseResult |> silent)\n                                then\n                                    AnsiConsole.MarkupLine(\"[yellow]No review notes found.[/]\")\n\n                            return Ok returnValue\n                        | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Open() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = openHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private checkpointHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let promotionSetIdRaw =\n                    parseResult.GetValue(Options.promotionSetId)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                match tryParseGuid promotionSetIdRaw ReviewError.InvalidPromotionSetId parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let referenceIdRaw = parseResult.GetValue(Options.referenceId)\n\n                    match tryParseGuid referenceIdRaw ReviewError.InvalidReferenceId parseResult with\n                    | Error error -> return Error error\n                    | Ok referenceId ->\n                        let! policySnapshotIdResult = resolvePolicySnapshotId parseResult graceIds promotionSetId\n\n                        match policySnapshotIdResult with\n                        | Error error -> return Error error\n                        | Ok policySnapshotId ->\n                            let parameters =\n                                Parameters.Review.ReviewCheckpointParameters(\n                                    PromotionSetId = promotionSetId.ToString(),\n                                    ReviewedUpToReferenceId = referenceId.ToString(),\n                                    PolicySnapshotId = policySnapshotId,\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    CorrelationId = graceIds.CorrelationId\n                                )\n\n                            return! Review.Checkpoint(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Checkpoint() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = checkpointHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private resolveHandlerImpl (parseResult: ParseResult) =\n        if parseResult |> verbose then printParseResult parseResult\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n        let promotionSetIdRaw =\n            parseResult.GetValue(Options.promotionSetId)\n            |> Option.ofObj\n            |> Option.defaultValue String.Empty\n\n        match tryParseGuid promotionSetIdRaw ReviewError.InvalidPromotionSetId parseResult with\n        | Error error -> Task.FromResult(Error error)\n        | Ok promotionSetId ->\n            let findingIdRaw = parseResult.GetValue(Options.findingId)\n\n            match tryParseGuid findingIdRaw ReviewError.InvalidFindingId parseResult with\n            | Error error -> Task.FromResult(Error error)\n            | Ok findingId ->\n                let approve = parseResult.GetValue(Options.approve)\n                let requestChanges = parseResult.GetValue(Options.requestChanges)\n\n                if approve = requestChanges then\n                    Task.FromResult(Error(GraceError.Create \"Specify exactly one of --approve or --request-changes.\" (getCorrelationId parseResult)))\n                else\n                    let resolutionState =\n                        if approve then\n                            FindingResolutionState.Approved\n                        else\n                            FindingResolutionState.NeedsChanges\n\n                    let note =\n                        parseResult.GetValue(Options.note)\n                        |> Option.ofObj\n                        |> Option.defaultValue String.Empty\n\n                    let parameters =\n                        Parameters.Review.ResolveFindingParameters(\n                            PromotionSetId = promotionSetId.ToString(),\n                            FindingId = findingId.ToString(),\n                            ResolutionState = getDiscriminatedUnionCaseName resolutionState,\n                            Note = note,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    Review.ResolveFinding(parameters)\n\n    let private resolveHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! resolveHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Resolve() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = resolveHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private deepenHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let promotionSetIdRaw =\n                    parseResult.GetValue(Options.promotionSetId)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                match tryParseGuid promotionSetIdRaw ReviewError.InvalidPromotionSetId parseResult with\n                | Error error -> return Error error\n                | Ok promotionSetId ->\n                    let chapterId =\n                        parseResult.GetValue(Options.chapterId)\n                        |> Option.ofObj\n                        |> Option.defaultValue String.Empty\n\n                    let parameters =\n                        Parameters.Review.DeepenReviewParameters(\n                            PromotionSetId = promotionSetId.ToString(),\n                            ChapterId = chapterId,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    return! Review.Deepen(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Deepen() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = deepenHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private reportShowHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                match resolveCandidateId parseResult with\n                | Error error -> return Error error\n                | Ok candidateId ->\n                    let parameters = buildCandidateProjectionParameters graceIds candidateId\n                    let! result = Review.GetReviewReport(parameters)\n\n                    match result with\n                    | Ok returnValue ->\n                        if\n                            not (parseResult |> json)\n                            && not (parseResult |> silent)\n                        then\n                            let markdown = renderReviewReportMarkdown returnValue.ReturnValue\n                            Console.WriteLine(markdown)\n\n                        return Ok returnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type ReportShow() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = reportShowHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private reportExportHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n\n                let outputFile =\n                    parseResult.GetValue(Options.outputFile)\n                    |> Option.ofObj\n                    |> Option.defaultValue String.Empty\n\n                if String.IsNullOrWhiteSpace outputFile then\n                    return Error(GraceError.Create \"Output file path is required.\" (getCorrelationId parseResult))\n                else\n                    let reportFormatRaw =\n                        parseResult.GetValue(Options.reportFormat)\n                        |> Option.ofObj\n                        |> Option.defaultValue String.Empty\n\n                    match parseReportExportFormat reportFormatRaw parseResult with\n                    | Error error -> return Error error\n                    | Ok reportFormat ->\n                        match resolveCandidateId parseResult with\n                        | Error error -> return Error error\n                        | Ok candidateId ->\n                            let parameters = buildCandidateProjectionParameters graceIds candidateId\n                            let! result = Review.GetReviewReport(parameters)\n\n                            match result with\n                            | Error error -> return Error error\n                            | Ok returnValue ->\n                                let report = returnValue.ReturnValue\n\n                                let content =\n                                    match reportFormat with\n                                    | Markdown -> renderReviewReportMarkdown report\n                                    | Json -> serializeReviewReportJson report\n\n                                let outputDirectory = Path.GetDirectoryName(outputFile)\n\n                                if not (String.IsNullOrWhiteSpace outputDirectory) then\n                                    Directory.CreateDirectory(outputDirectory)\n                                    |> ignore\n\n                                do! File.WriteAllTextAsync(outputFile, content)\n\n                                if\n                                    not (parseResult |> json)\n                                    && not (parseResult |> silent)\n                                then\n                                    let formatText =\n                                        match reportFormat with\n                                        | Markdown -> \"markdown\"\n                                        | Json -> \"json\"\n\n                                    AnsiConsole.MarkupLine($\"[green]Review report exported ({formatText}) to[/] {Markup.Escape(outputFile)}\")\n\n                                return Ok returnValue\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type ReportExport() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, _: CancellationToken) : Task<int> =\n            task {\n                let! result = reportExportHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let reviewCommand = new Command(\"review\", Description = \"Promotion-set review operations plus candidate report output.\")\n\n        let inboxCommand = new Command(\"inbox\", Description = \"Show review inbox (stub).\")\n\n        inboxCommand\n        |> addOption Options.targetBranch\n        |> addCommonOptions\n        |> ignore\n\n        inboxCommand.Action <- new Inbox()\n        reviewCommand.Subcommands.Add(inboxCommand)\n\n        let openCommand = new Command(\"open\", Description = \"Open review notes for a promotion set.\")\n\n        openCommand\n        |> addOption Options.promotionSetId\n        |> addCommonOptions\n        |> ignore\n\n        openCommand.Action <- new Open()\n        reviewCommand.Subcommands.Add(openCommand)\n\n        let checkpointCommand =\n            new Command(\"checkpoint\", Description = \"Record a review checkpoint for a promotion set.\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.referenceId\n            |> addOption Options.policySnapshotId\n            |> addCommonOptions\n\n        checkpointCommand.Action <- new Checkpoint()\n        reviewCommand.Subcommands.Add(checkpointCommand)\n\n        let resolveCommand =\n            new Command(\"resolve\", Description = \"Resolve a review finding for a promotion set.\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.findingId\n            |> addOption Options.approve\n            |> addOption Options.requestChanges\n            |> addOption Options.note\n            |> addCommonOptions\n\n        resolveCommand.Action <- new Resolve()\n        reviewCommand.Subcommands.Add(resolveCommand)\n\n        let deepenCommand =\n            new Command(\"deepen\", Description = \"Request deeper analysis (stub).\")\n            |> addOption Options.promotionSetId\n            |> addOption Options.chapterId\n            |> addCommonOptions\n\n        deepenCommand.Action <- new Deepen()\n        reviewCommand.Subcommands.Add(deepenCommand)\n\n        let reportCommand = new Command(\"report\", Description = \"Generate candidate-first unified review reports.\")\n\n        let reportShowCommand =\n            new Command(\"show\", Description = \"Show review report sections in deterministic markdown order.\")\n            |> addOption Options.candidateId\n            |> addCommonOptions\n\n        reportShowCommand.Action <- new ReportShow()\n        reportCommand.Subcommands.Add(reportShowCommand)\n\n        let reportExportCommand =\n            new Command(\"export\", Description = \"Export review report as markdown or json.\")\n            |> addOption Options.candidateId\n            |> addOption Options.reportFormat\n            |> addOption Options.outputFile\n            |> addCommonOptions\n\n        reportExportCommand.Action <- new ReportExport()\n        reportCommand.Subcommands.Add(reportExportCommand)\n        reviewCommand.Subcommands.Add(reportCommand)\n\n        reviewCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/Services.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen Microsoft.Extensions\nopen FSharp.Collections\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen NodaTime\nopen NodaTime.Text\nopen Spectre.Console\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Parsing\nopen System.Diagnostics\nopen System.Globalization\nopen System.IO\nopen System.IO.Compression\nopen System.IO.Enumeration\nopen System.IO.Pipelines\nopen System.Linq\nopen System.Security.Cryptography\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\nopen System.Reactive.Linq\nopen System.Threading\nopen Grace.Shared.Parameters\nopen Grace.Shared.Parameters.Storage\nopen System.Runtime.Intrinsics.Arm\n\nmodule Services =\n\n    let mutable lockObject = new Lock()\n\n    /// Utility method to write to the console using color.\n    let logToAnsiConsole (color: string) message =\n        lock lockObject (fun () ->\n            AnsiConsole.MarkupLine $\"[{color}]{getCurrentInstantExtended ()} {Environment.CurrentManagedThreadId:X2} {Markup.Escape(message)}[/]\")\n\n    /// A cache of paths that we've already decided to ignore or not.\n    let private shouldIgnoreCache = ConcurrentDictionary<FilePath, bool>()\n\n    // This section is \"borrowed\" from Common.CLI.fs, because Services.CLI.fs comes before Common.CLI.fs in the build order.\n\n    /// Checks if the output format from the command line is a specific format.\n    let private isOutputFormat (outputFormat: string) (parseResult: ParseResult) =\n        if parseResult\n            .ToString()\n            .IndexOf($\"<{outputFormat}>\", StringComparison.InvariantCultureIgnoreCase) > 0 then\n            true\n        else if outputFormat = \"Normal\" then\n            true\n        else\n            false\n\n    /// GraceWatchStatus defines the schema for the inter-process communication (IPC) file that lets Grace know if `grace watch` is already running.\n    ///\n    /// It's written by `grace watch`. It holds everything required to allow other instances of Grace to skip checking current status.\n    [<Struct>]\n    type GraceWatchStatus =\n        {\n            UpdatedAt: Instant\n            RootDirectoryId: DirectoryVersionId\n            RootDirectorySha256Hash: Sha256Hash\n            LastFileUploadInstant: Instant\n            LastDirectoryVersionInstant: Instant\n            DirectoryIds: HashSet<DirectoryVersionId>\n        }\n\n        static member Default =\n            {\n                UpdatedAt = Instant.MinValue\n                RootDirectoryId = Guid.Empty\n                RootDirectorySha256Hash = Sha256Hash String.Empty\n                LastFileUploadInstant = Instant.MinValue\n                LastDirectoryVersionInstant = Instant.MinValue\n                DirectoryIds = HashSet<DirectoryVersionId>()\n            }\n\n    let mutable graceWatchStatusUpdateTime = Instant.MinValue\n    let mutable parseResult: ParseResult = null\n    let mutable private invocationCorrelationId: CorrelationId option = None\n\n    let resetInvocationCorrelationId () = invocationCorrelationId <- None\n\n    // Extension methods for dealing with local file changes.\n    type DirectoryVersion with\n        /// Gets the full path for this file in the working directory.\n        member this.FullName = Path.Combine(Current().RootDirectory, $\"{this.RelativePath}\")\n\n    // Extension methods for dealing with local files.\n    type LocalDirectoryVersion with\n        /// Gets the full path for this file in the working directory.\n        member this.FullName = Path.Combine(Current().RootDirectory, $\"{this.RelativePath}\")\n        /// Gets a DirectoryInfo instance for the parent directory of this local file.\n        member this.DirectoryInfo = DirectoryInfo(this.FullName)\n\n    // Extension methods for dealing with local files.\n    type LocalFileVersion with\n        /// Gets the full path for this file in the working directory.\n        member this.FullName = getNativeFilePath (Path.Combine(Current().RootDirectory, $\"{this.RelativePath}\"))\n\n        /// Gets the full working directory path for this file.\n        member this.FullRelativePath = FileInfo(this.FullName).DirectoryName\n        /// Gets a FileInfo instance for this local file.\n        member this.FileInfo = FileInfo(this.FullName)\n\n        /// Gets the RelativeDirectory for this file.\n        member this.RelativeDirectory = Path.GetRelativePath(Current().RootDirectory, this.FileInfo.DirectoryName)\n\n        /// Gets the full name of the object file for this LocalFileVersion.\n        member this.FullObjectPath = getNativeFilePath (Path.Combine(Current().ObjectDirectory, this.RelativePath, this.GetObjectFileName))\n\n    /// Flag to determine if we should do case-insensitive file name processing on the current platform.\n    let ignoreCase = runningOnWindows\n\n    /// Returns true if fileToCheck matches this graceIgnoreEntry; otherwise returns false.\n    let checkIgnoreLineAgainstFile (fileToCheck: FilePath) (graceIgnoreEntry: string) =\n        let fileName = Path.GetFileName(fileToCheck)\n\n        let ignoreEntryMatches = FileSystemName.MatchesSimpleExpression(graceIgnoreEntry, fileName, ignoreCase)\n\n        ignoreEntryMatches\n\n    /// Returns true if directory matches this graceIgnoreEntry; otherwise returns false.\n    let checkIgnoreLineAgainstDirectory (directoryInfoToCheck: DirectoryInfo) (graceIgnoreEntry: string) =\n        let normalizedDirectoryPath =\n            if Path.EndsInDirectorySeparator(directoryInfoToCheck.FullName) then\n                normalizeFilePath directoryInfoToCheck.FullName\n            else\n                normalizeFilePath (directoryInfoToCheck.FullName + \"/\")\n\n        if FileSystemName.MatchesSimpleExpression(graceIgnoreEntry, normalizedDirectoryPath, ignoreCase) then\n            //logToAnsiConsole Colors.Changed $\"checkIgnoreLineAgainstDirectory: directory '{normalizedDirectoryPath}' matches ignore entry '{graceIgnoreEntry}'.\"\n            true\n        else\n            //logToAnsiConsole\n            //    Colors.Verbose\n            //    $\"checkIgnoreLineAgainstDirectory: directory '{normalizedDirectoryPath}' does not match ignore entry '{graceIgnoreEntry}'.\"\n\n            false\n\n    /// Returns true if filePath should be ignored by Grace, otherwise returns false.\n    let shouldIgnoreFile (filePath: FilePath) =\n        let mutable shouldIgnore = false\n        let wasAlreadyCached = shouldIgnoreCache.TryGetValue(filePath, &shouldIgnore)\n        //logToConsole $\"In shouldIgnoreFile: filePath: {filePath}; wasAlreadyCached: {wasAlreadyCached}; shouldIgnore: {shouldIgnore}\"\n        if wasAlreadyCached then\n            shouldIgnore\n        else\n            // Ignore it if:\n            //   it's in the .grace directory, or\n            //   it's the Grace Status file, or\n            //   it's a Grace-owned temporary file, or\n            //   it's a directory itself, or\n            //   it matches something in graceignore.txt.\n            let fileInfo = FileInfo(filePath)\n\n            let shouldIgnoreThisFile =\n                filePath.StartsWith(Current().GraceDirectory, StringComparison.InvariantCultureIgnoreCase) // it's in the /.grace directory\n                || filePath.Equals(Current().GraceStatusFile, StringComparison.InvariantCultureIgnoreCase) // it's the Grace local state DB\n                || filePath.Equals(Current().GraceStatusFile + \"-wal\", StringComparison.InvariantCultureIgnoreCase) // sqlite WAL\n                || filePath.Equals(Current().GraceStatusFile + \"-shm\", StringComparison.InvariantCultureIgnoreCase) // sqlite SHM\n                || filePath.Equals(Current().GraceStatusFile + \"-journal\", StringComparison.InvariantCultureIgnoreCase) // sqlite journal\n                || filePath.EndsWith(\".gracetmp\") // it's a Grace temporary file\n                || Directory.Exists(filePath) // it's a directory\n                //|| fileInfo.Attributes.HasFlag(FileAttributes.Temporary)                                          // it's temporary - why doesn't this work\n                || Current().GraceDirectoryIgnoreEntries // one of the directories in the path matches a directory ignore line\n                   |> Array.exists (fun graceIgnoreLine -> checkIgnoreLineAgainstDirectory fileInfo.Directory graceIgnoreLine)\n                || Current().GraceDirectoryIgnoreEntries // the file name matches a directory ignore line (which is weird, but possible)\n                   |> Array.exists (fun graceIgnoreLine -> checkIgnoreLineAgainstFile filePath graceIgnoreLine)\n                || Current().GraceFileIgnoreEntries // the file name matches a file ignore line\n                   |> Array.exists (fun graceIgnoreLine -> checkIgnoreLineAgainstFile filePath graceIgnoreLine)\n\n\n            //logToAnsiConsole Colors.Verbose $\"In shouldIgnoreFile: filePath: {filePath}; shouldIgnore: {shouldIgnoreThisFile}\"\n            shouldIgnoreCache.TryAdd(filePath, shouldIgnoreThisFile)\n            |> ignore\n\n            shouldIgnoreThisFile\n\n    let private notString = \"not \"\n\n    /// Returns true if directoryPath should be ignored by Grace, otherwise returns false.\n    let shouldIgnoreDirectory (directoryPath: string) =\n        let mutable shouldIgnore = false\n        let wasAlreadyCached = shouldIgnoreCache.TryGetValue(directoryPath, &shouldIgnore)\n\n        if wasAlreadyCached then\n            shouldIgnore\n        else\n            let directoryInfo = DirectoryInfo(directoryPath)\n\n            let shouldIgnoreDirectory =\n                directoryInfo.FullName.StartsWith(Current().GraceDirectory)\n                || Current()\n                    .GraceDirectoryIgnoreEntries.Any(fun graceIgnoreLine -> checkIgnoreLineAgainstDirectory directoryInfo graceIgnoreLine)\n\n            shouldIgnoreCache.TryAdd(directoryPath, shouldIgnoreDirectory)\n            |> ignore\n            //logToAnsiConsole Colors.Verbose $\"In shouldIgnoreDirectory: directoryPath: {directoryPath}; shouldIgnore: {shouldIgnoreDirectory}\"\n            shouldIgnoreDirectory\n\n    /// Returns true if directoryPath should not be ignored by Grace, otherwise returns false.\n    let shouldNotIgnoreDirectory (directoryPath: string) = not <| shouldIgnoreDirectory directoryPath\n\n    /// Creates a LocalFileVersion for the given FileInfo instance.\n    let createLocalFileVersion (fileInfo: FileInfo) =\n        task {\n            if fileInfo.Exists then\n                try\n                    let relativePath = Path.GetRelativePath(Current().RootDirectory, fileInfo.FullName)\n\n                    use stream = fileInfo.Open(fileStreamOptionsRead)\n\n                    let! isBinary = isBinaryFile stream\n\n                    stream.Position <- 0\n                    let! sha256Hash = computeSha256ForFile stream relativePath\n\n                    let returnValue =\n                        LocalFileVersion.Create\n                            relativePath\n                            sha256Hash\n                            isBinary\n                            fileInfo.Length\n                            (Instant.FromDateTimeUtc(fileInfo.LastWriteTimeUtc))\n                            true\n                            fileInfo.LastWriteTimeUtc\n\n                    return Some returnValue\n                with\n                | ex ->\n                    logToAnsiConsole Colors.Error $\"Exception in createLocalFileVersion for file {fileInfo.FullName}:\"\n                    logToAnsiConsole Colors.Error $\"{ExceptionResponse.Create ex}\"\n                    return None\n            else\n                return None\n        }\n\n    /// Gets the LocalDirectoryVersion for the root directory of the repository from GraceStatus.\n    let getRootDirectoryVersion (graceStatus: GraceStatus) =\n        graceStatus.Index.Values.FirstOrDefault(\n            (fun localDirectoryVersion -> localDirectoryVersion.RelativePath = Constants.RootDirectoryPath),\n            LocalDirectoryVersion.Default\n        )\n\n    let localWriteTimes = ConcurrentDictionary<FileSystemEntryType * RelativePath, DateTime>()\n\n    /// Gets a dictionary of local paths and their last write times.\n    let rec getWorkingDirectoryWriteTimes (directoryInfo: DirectoryInfo) =\n        if shouldNotIgnoreDirectory directoryInfo.FullName then\n            // Add the current directory to the lookup dictionary\n            let directoryFullPath = RelativePath(normalizeFilePath (Path.GetRelativePath(Current().RootDirectory, directoryInfo.FullName)))\n\n            localWriteTimes.AddOrUpdate(\n                (FileSystemEntryType.Directory, directoryFullPath),\n                (fun _ -> directoryInfo.LastWriteTimeUtc),\n                (fun _ _ -> directoryInfo.LastWriteTimeUtc)\n            )\n            |> ignore\n\n            // Add each file to the lookup dictionary\n            for f in\n                directoryInfo\n                    .GetFiles()\n                    .Where(fun f -> not <| shouldIgnoreFile f.FullName) do\n                let fileFullPath = RelativePath(normalizeFilePath (Path.GetRelativePath(Current().RootDirectory, f.FullName)))\n\n                localWriteTimes.AddOrUpdate((FileSystemEntryType.File, fileFullPath), (fun _ -> f.LastWriteTimeUtc), (fun _ _ -> f.LastWriteTimeUtc))\n                |> ignore\n\n            // Call recursively for each subdirectory\n            let parallelLoopResult =\n                Parallel.ForEach(directoryInfo.GetDirectories(), Constants.ParallelOptions, (fun d -> getWorkingDirectoryWriteTimes d |> ignore))\n\n            if parallelLoopResult.IsCompleted then\n                ()\n            else\n                printfn $\"Failed while gathering local write times.\"\n\n        localWriteTimes\n\n    let private getLocalStateDbPath () = Current().GraceStatusFile\n\n    /// Reads only GraceStatus meta fields (no index).\n    let readGraceStatusMeta () =\n        task {\n            let! meta = LocalStateDb.readStatusMeta (getLocalStateDbPath ())\n            return\n                {\n                    GraceStatus.Default with\n                        RootDirectoryId = meta.RootDirectoryId\n                        RootDirectorySha256Hash = meta.RootDirectorySha256Hash\n                        LastSuccessfulFileUpload = meta.LastSuccessfulFileUpload\n                        LastSuccessfulDirectoryVersionUpload = meta.LastSuccessfulDirectoryVersionUpload\n                }\n        }\n\n    /// Reads the full GraceStatus snapshot including the index.\n    let readGraceStatusSnapshot () = LocalStateDb.readStatusSnapshot (getLocalStateDbPath ())\n\n    /// Retrieves the Grace status snapshot (compatibility wrapper).\n    let readGraceStatusFile () = readGraceStatusSnapshot ()\n\n    /// Writes the full Grace status snapshot to disk.\n    let writeGraceStatusFile (graceStatus: GraceStatus) =\n        LocalStateDb.replaceStatusSnapshot (getLocalStateDbPath ()) graceStatus\n\n    /// Applies incremental Grace status updates to the local DB.\n    let applyGraceStatusIncremental\n        (graceStatus: GraceStatus)\n        (newDirectoryVersions: IEnumerable<LocalDirectoryVersion>)\n        (differences: IEnumerable<FileSystemDifference>)\n        =\n        LocalStateDb.applyStatusIncremental (getLocalStateDbPath ()) graceStatus newDirectoryVersions differences\n\n    /// Upserts new directory versions into the object cache tables.\n    let upsertObjectCache (newDirectoryVersions: IEnumerable<LocalDirectoryVersion>) =\n        LocalStateDb.upsertObjectCache (getLocalStateDbPath ()) newDirectoryVersions\n\n    /// Compared the repository's working directory against the Grace index file and returns the differences.\n    let scanForDifferences (previousGraceStatus: GraceStatus) =\n        task {\n            try\n                let lookupCache = Dictionary<FileSystemEntryType * RelativePath, (DateTime * Sha256Hash)>()\n\n                let differences = ConcurrentStack<FileSystemDifference>()\n\n                let mutable fileCount = 0\n                // Create an indexed lookup table of path -> lastWriteTimeUtc from the Grace Status index.\n                for kvp in previousGraceStatus.Index do\n                    let directoryVersion = kvp.Value\n\n                    lookupCache.TryAdd(\n                        (FileSystemEntryType.Directory, directoryVersion.RelativePath),\n                        (directoryVersion.LastWriteTimeUtc, directoryVersion.Sha256Hash)\n                    )\n                    |> ignore\n\n                    for file in directoryVersion.Files do\n                        fileCount <- fileCount + 1\n\n                        lookupCache.TryAdd((FileSystemEntryType.File, file.RelativePath), (file.LastWriteTimeUtc, file.Sha256Hash))\n                        |> ignore\n\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole\n                        Colors.Verbose\n                        $\"scanForDifferences: previousGraceStatus contains {previousGraceStatus.Index.Count} DirectoryVersion entries, and {fileCount} files.\"\n\n                // Get an indexed lookup dictionary of path -> lastWriteTimeUtc from the working directory.\n                let localWriteTimes = getWorkingDirectoryWriteTimes (DirectoryInfo(Current().RootDirectory))\n\n                // Loop through the working directory list and compare it to the Grace Status index.\n                for kvp in localWriteTimes do\n                    let ((fileSystemEntryType, relativePath), lastWriteTimeUtc) = kvp.Deconstruct()\n                    // Check for additions\n                    if\n                        not\n                        <| lookupCache.ContainsKey((fileSystemEntryType, relativePath))\n                    then\n                        // This is new file or directory.\n                        differences.Push(FileSystemDifference.Create Add fileSystemEntryType relativePath)\n\n                    // Check for changes\n                    if lookupCache.ContainsKey((fileSystemEntryType, relativePath)) then\n                        let (knownLastWriteTimeUtc, existingSha256Hash) = lookupCache[(fileSystemEntryType, relativePath)]\n                        // Has the LastWriteTimeUtc changed from the one in GraceStatus?\n                        if fileSystemEntryType.IsFile\n                           && lastWriteTimeUtc <> knownLastWriteTimeUtc then\n                            // If it's a directory, ignore it. I don't care when the local directory was created vs. the one stored in GraceIndex.\n                            // If this is a file, then check that the contents have actually changed.\n                            let fileInfo = FileInfo(Path.Combine(Current().RootDirectory, relativePath))\n\n                            match! createLocalFileVersion fileInfo with\n                            | Some newFileVersion ->\n                                if newFileVersion.Sha256Hash <> existingSha256Hash then\n                                    differences.Push(FileSystemDifference.Create Change fileSystemEntryType relativePath)\n\n                                    if parseResult |> isOutputFormat \"Verbose\" then\n                                        logToAnsiConsole\n                                            Colors.Verbose\n                                            $\"scanForDifferences: Found change in file: {relativePath}; existing Sha256Hash: {getShortSha256Hash existingSha256Hash}; new Sha256Hash: {getShortSha256Hash newFileVersion.Sha256Hash}.\"\n                            | None -> ()\n\n                // Check for deletions\n                for keyValuePair in lookupCache do\n                    let (fileSystemEntryType, relativePath) = keyValuePair.Key\n                    let (knownLastWriteTimeUtc, existingSha256Hash) = keyValuePair.Value\n\n                    if\n                        not\n                        <| localWriteTimes.ContainsKey((fileSystemEntryType, relativePath))\n                    then\n                        if parseResult |> isOutputFormat \"Verbose\" then\n                            logToAnsiConsole Colors.Verbose $\"scanForDifferences: Deletion found: {relativePath}.\"\n\n                        differences.Push(FileSystemDifference.Create Delete fileSystemEntryType relativePath)\n\n                return differences.ToList()\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"{ExceptionResponse.Create ex}\"\n                return List<FileSystemDifference>()\n        }\n\n    //let processedThings = ConcurrentQueue<string>()\n    let mutable newDirectoryVersionCount = 0\n    let mutable existingDirectoryVersionCount = 0\n\n    /// Gathers all of the LocalDirectoryVersions and LocalFileVersions for the requested directory and its subdirectories, and returns them along with the Sha256Hash of the requested directory.\n    let rec collectDirectoriesAndFiles\n        (relativeDirectoryPath: RelativePath)\n        (previousDirectoryVersions: Dictionary<RelativePath, LocalDirectoryVersion>)\n        (newGraceStatus: GraceStatus)\n        (parseResult: ParseResult)\n        =\n\n        /// Gets a list of subdirectories and files from a specific directory\n        let getDirectoryContents (previousDirectoryVersions: Dictionary<RelativePath, LocalDirectoryVersion>) (directoryInfo: DirectoryInfo) =\n            task {\n                try\n                    let files = ConcurrentQueue<LocalFileVersion>()\n                    let directories = ConcurrentQueue<LocalDirectoryVersion>()\n\n                    // Create LocalFileVersion instances for each file in this directory.\n                    do!\n                        Parallel.ForEachAsync(\n                            directoryInfo\n                                .GetFiles()\n                                .Where(fun f -> not <| shouldIgnoreFile f.FullName),\n                            Constants.ParallelOptions,\n                            (fun fileInfo continuationToken ->\n                                ValueTask(\n                                    task {\n                                        try\n                                            match! createLocalFileVersion fileInfo with\n                                            | Some fileVersion -> files.Enqueue(fileVersion)\n                                            | None -> ()\n                                        with\n                                        | ex ->\n                                            logToAnsiConsole Colors.Error $\"Exception in getDirectoryContents (Parallel.ForEachAsync):\"\n                                            logToAnsiConsole Colors.Error $\"{ExceptionResponse.Create ex}\"\n                                    }\n                                ))\n                        )\n\n                    // Create or reuse existing LocalDirectoryVersion instances for each subdirectory in this directory.\n                    let defaultDirectoryVersion = KeyValuePair(String.Empty, LocalDirectoryVersion.Default)\n\n                    do!\n                        Parallel.ForEachAsync(\n                            directoryInfo\n                                .GetDirectories()\n                                .Where(fun d -> shouldNotIgnoreDirectory d.FullName),\n                            Constants.ParallelOptions,\n                            (fun subdirectoryInfo continuationToken ->\n                                ValueTask(\n                                    task {\n                                        try\n\n                                            let subdirectoryRelativePath = Path.GetRelativePath(Current().RootDirectory, subdirectoryInfo.FullName)\n\n\n                                            let! (subdirectoryVersions: List<LocalDirectoryVersion>, filesInSubdirectory: List<LocalFileVersion>, sha256Hash) =\n                                                collectDirectoriesAndFiles subdirectoryRelativePath previousDirectoryVersions newGraceStatus parseResult\n\n                                            // Check if we already have a LocalDirectoryVersion for this subdirectory.\n                                            let existingSubdirectoryVersion =\n                                                previousDirectoryVersions\n                                                    .FirstOrDefault(\n                                                        (fun existingDirectoryVersion ->\n                                                            existingDirectoryVersion.Key = normalizeFilePath subdirectoryRelativePath),\n                                                        defaultValue = defaultDirectoryVersion\n                                                    )\n                                                    .Value\n\n                                            // Check if we already have this exact SHA-256 hash for this relative path; if so, keep the existing SubdirectoryVersion and its Guid.\n                                            // If the DirectoryId is Guid.Empty (from LocalDirectoryVersion.Default), or the Sha256Hash doesn't match, create a new LocalDirectoryVersion reflecting the changes.\n                                            if existingSubdirectoryVersion.DirectoryVersionId = Guid.Empty\n                                               || existingSubdirectoryVersion.Sha256Hash\n                                                  <> sha256Hash then\n                                                let directoryIds =\n                                                    subdirectoryVersions\n                                                        .OrderBy(fun d -> d.RelativePath)\n                                                        .Select(fun d -> d.DirectoryVersionId)\n                                                        .ToList()\n\n                                                let subdirectoryVersion =\n                                                    LocalDirectoryVersion.Create\n                                                        (Guid.NewGuid())\n                                                        (Current().OwnerId)\n                                                        (Current().OrganizationId)\n                                                        (Current().RepositoryId)\n                                                        (RelativePath(normalizeFilePath subdirectoryRelativePath))\n                                                        sha256Hash\n                                                        directoryIds\n                                                        filesInSubdirectory\n                                                        (getLocalDirectorySize filesInSubdirectory)\n                                                        subdirectoryInfo.LastWriteTimeUtc\n                                                //processedThings.Enqueue($\"New      {subdirectoryVersion.RelativePath}\")\n                                                newGraceStatus.Index.TryAdd(subdirectoryVersion.DirectoryVersionId, subdirectoryVersion)\n                                                |> ignore\n\n                                                Interlocked.Increment(&newDirectoryVersionCount)\n                                                |> ignore\n\n                                                directories.Enqueue(subdirectoryVersion)\n                                            else\n                                                //processedThings.Enqueue($\"Existing {existingSubdirectoryVersion.RelativePath}\")\n                                                newGraceStatus.Index.TryAdd(existingSubdirectoryVersion.DirectoryVersionId, existingSubdirectoryVersion)\n                                                |> ignore\n\n                                                Interlocked.Increment(&existingDirectoryVersionCount)\n                                                |> ignore\n\n                                                directories.Enqueue(existingSubdirectoryVersion)\n                                        with\n                                        | ex -> logToAnsiConsole Colors.Error $\"Exception in getDirectoryContents: {ExceptionResponse.Create ex}\"\n                                    }\n                                ))\n                        )\n\n                    return\n                        (directories\n                            .OrderBy(fun d -> d.RelativePath)\n                             .ToList(),\n                         files.OrderBy(fun d -> d.RelativePath).ToList())\n                with\n                | ex ->\n                    logToAnsiConsole Colors.Error $\"Exception in getDirectoryContents:\"\n                    logToAnsiConsole Colors.Error $\"{ExceptionResponse.Create ex}\"\n                    return (List<LocalDirectoryVersion>(), List<LocalFileVersion>())\n            }\n\n        task {\n            try\n                let directoryInfo = DirectoryInfo(Path.Combine(Current().RootDirectory, relativeDirectoryPath))\n\n                let! (directories, files) = getDirectoryContents previousDirectoryVersions directoryInfo\n                //for file in files do processedThings.Enqueue(file.RelativePath)\n\n                let sha256Hash = computeSha256ForDirectory relativeDirectoryPath directories files\n\n                return (directories, files, sha256Hash)\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"Exception in collectDirectoriesAndFiles: {ExceptionResponse.Create ex}\"\n                return (List<LocalDirectoryVersion>(), List<LocalFileVersion>(), Sha256Hash.Empty)\n        }\n\n    /// Creates the Grace index file by scanning the repository's working directory.\n    let createNewGraceStatusFile (previousGraceStatus: GraceStatus) (parseResult: ParseResult) =\n        task {\n            try\n                // Start with a new GraceStatus instance.\n                let newGraceStatus = GraceStatus.Default\n                let rootDirectoryInfo = DirectoryInfo(Current().RootDirectory)\n\n                // Get the previous GraceStatus index values into a Dictionary for faster lookup.\n                let previousDirectoryVersions = Dictionary<RelativePath, LocalDirectoryVersion>()\n\n                for kvp in previousGraceStatus.Index do\n                    if\n                        not\n                        <| previousDirectoryVersions.TryAdd(kvp.Value.RelativePath, kvp.Value)\n                    then\n                        logToAnsiConsole Colors.Error $\"createNewGraceStatusFile: Failed to add {kvp.Value.RelativePath} to previousDirectoryVersions.\"\n\n                let! (subdirectoriesInRootDirectory, filesInRootDirectory, rootSha256Hash) =\n                    collectDirectoriesAndFiles Constants.RootDirectoryPath previousDirectoryVersions newGraceStatus parseResult\n\n                //let getBySha256HashParameters = GetBySha256HashParameters(RepositoryId = $\"{Current().RepositoryId}\", Sha256Hash = rootSha256Hash)\n                //let! directoryId = Directory.GetBySha256Hash(getBySha256HashParameters)\n\n                // Check for existing root directory version so we don't update the Guid if it already exists.\n                let rootDirectoryVersion =\n                    let previousRootDirectoryVersion =\n                        previousDirectoryVersions\n                            .FirstOrDefault(\n                                (fun existingDirectoryVersion -> existingDirectoryVersion.Key = Constants.RootDirectoryPath),\n                                defaultValue = KeyValuePair(String.Empty, LocalDirectoryVersion.Default)\n                            )\n                            .Value\n\n                    if previousRootDirectoryVersion.Sha256Hash = rootSha256Hash then\n                        previousRootDirectoryVersion\n                    else\n                        let subdirectoryIds =\n                            subdirectoriesInRootDirectory\n                                .OrderBy(fun d -> d.RelativePath)\n                                .Select(fun d -> d.DirectoryVersionId)\n                                .ToList()\n\n                        LocalDirectoryVersion.Create\n                            (Guid.NewGuid())\n                            (Current().OwnerId)\n                            (Current().OrganizationId)\n                            (Current().RepositoryId)\n                            (RelativePath(normalizeFilePath Constants.RootDirectoryPath))\n                            (Sha256Hash rootSha256Hash)\n                            subdirectoryIds\n                            filesInRootDirectory\n                            (getLocalDirectorySize filesInRootDirectory)\n                            rootDirectoryInfo.LastWriteTimeUtc\n\n                newGraceStatus.Index.TryAdd(Guid.Parse($\"{rootDirectoryVersion.DirectoryVersionId}\"), rootDirectoryVersion)\n                |> ignore\n\n                //let sb = StringBuilder(processedThings.Count)\n                //for dir in processedThings.OrderBy(fun d -> d) do\n                //    sb.AppendLine(dir) |> ignore\n                //do! File.WriteAllTextAsync(@$\"C:\\Intel\\ProcessedThings{sb.Length}.txt\", sb.ToString())\n                let rootDirectoryVersion = getRootDirectoryVersion newGraceStatus\n\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole Colors.Verbose $\"Finished createNewGraceStatusFile. newGraceStatus.Index.Count: {newGraceStatus.Index.Count}.\"\n\n                let newGraceStatus =\n                    { newGraceStatus with RootDirectoryId = rootDirectoryVersion.DirectoryVersionId; RootDirectorySha256Hash = rootDirectoryVersion.Sha256Hash }\n\n                return newGraceStatus\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"Exception in createNewGraceStatusFile: {ExceptionResponse.Create ex}\"\n                return GraceStatus.Default\n        }\n\n    /// Adds a LocalDirectoryVersion to the local object cache.\n    let addDirectoryToObjectCache (localDirectoryVersion: LocalDirectoryVersion) =\n        task {\n            let! exists =\n                LocalStateDb.isDirectoryVersionInObjectCache\n                    (getLocalStateDbPath ())\n                    localDirectoryVersion.DirectoryVersionId\n\n            if not exists then\n                let allFilesExist =\n                    localDirectoryVersion.Files\n                    |> Seq.forall (fun file -> File.Exists(Path.Combine(Current().ObjectDirectory, file.RelativeDirectory, file.GetObjectFileName)))\n\n                if allFilesExist then\n                    do! upsertObjectCache [ localDirectoryVersion ]\n                    return Ok()\n                else\n                    return Error \"Directory could not be added to object cache. All files do not exist in /objects directory.\"\n            else\n                return Ok()\n        }\n\n    /// Removes a directory from the local object cache.\n    let removeDirectoryFromObjectCache (directoryId: DirectoryVersionId) =\n        task {\n            do! LocalStateDb.removeObjectCacheDirectory (getLocalStateDbPath ()) directoryId\n        }\n\n    /// Downloads files from object storage that aren't already present in the local object cache.\n    let downloadFilesFromObjectStorage (getDownloadUriParameters: GetDownloadUriParameters) (files: IEnumerable<LocalFileVersion>) (correlationId: string) =\n        task {\n            match Current().ObjectStorageProvider with\n            | ObjectStorageProvider.Unknown -> return Ok()\n            | AzureBlobStorage ->\n                let results =\n                    files.ToArray()\n                    |> Array.where (fun f -> not <| File.Exists(f.FullObjectPath))\n                    |> Array.Parallel.map (fun f ->\n                        (task {\n                            let parameters = GetDownloadUriParameters()\n                            parameters.OwnerId <- getDownloadUriParameters.OwnerId\n                            parameters.OwnerName <- getDownloadUriParameters.OwnerName\n                            parameters.OrganizationId <- getDownloadUriParameters.OrganizationId\n                            parameters.OrganizationName <- getDownloadUriParameters.OrganizationName\n                            parameters.RepositoryId <- getDownloadUriParameters.RepositoryId\n                            parameters.RepositoryName <- getDownloadUriParameters.RepositoryName\n                            parameters.FileVersion <- f.ToFileVersion\n                            parameters.CorrelationId <- getDownloadUriParameters.CorrelationId\n\n                            return! Storage.GetFileFromObjectStorage parameters correlationId\n                        })\n                            .Result)\n\n                let (results, errors) =\n                    results\n                    |> Array.partition (fun result ->\n                        match result with\n                        | Ok _ -> true\n                        | Error _ -> false)\n\n                if errors.Count() > 0 then\n                    let sb = stringBuilderPool.Get()\n\n                    try\n                        sb.Append($\"Some files could not be downloaded from object storage.{Environment.NewLine}\")\n                        |> ignore\n\n                        errors\n                        |> Seq.iter (fun e ->\n                            match e with\n                            | Ok _ -> ()\n                            | Error e ->\n                                sb.AppendLine($\"{e.Error}{Environment.NewLine}{serialize e.Properties}\")\n                                |> ignore)\n\n                        return Error(sb.ToString())\n                    finally\n                        stringBuilderPool.Return(sb) |> ignore\n                else\n                    return Ok()\n            | AWSS3 -> return Ok()\n            | GoogleCloudStorage -> return Ok()\n        }\n\n    /// Uploads all new or changed files from a directory to object storage.\n    let uploadFilesToObjectStorage (parameters: GetUploadMetadataForFilesParameters) =\n        task {\n            match Current().ObjectStorageProvider with\n            | ObjectStorageProvider.Unknown -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n            | AzureBlobStorage ->\n                //logToAnsiConsole Colors.Verbose $\"Uploading {fileVersions.Count()} files to object storage.\"\n                if parameters.FileVersions.Count() > 0 then\n                    match! Storage.GetUploadMetadataForFiles parameters with\n                    | Ok graceReturnValue ->\n                        let filesToUpload = graceReturnValue.ReturnValue\n                        //logToAnsiConsole Colors.Verbose $\"In Services.uploadFilesToObjectStorage(): filesToUpload: {serialize filesToUpload}.\"\n                        let errors = ConcurrentQueue<GraceError>()\n\n                        do!\n                            Parallel.ForEachAsync(\n                                filesToUpload,\n                                Constants.ParallelOptions,\n                                (fun uploadMetadata ct ->\n                                    ValueTask(\n                                        task {\n                                            let fileVersion =\n                                                (parameters.FileVersions.First(fun fileVersion -> fileVersion.Sha256Hash = uploadMetadata.Sha256Hash))\n                                            //logToAnsiConsole Colors.Verbose $\"In Services.uploadFilesToObjectStorage(): Uploading {fileVersion.GetObjectFileName} to object storage.\"\n                                            match!\n                                                Storage.SaveFileToObjectStorage\n                                                    (RepositoryId.Parse(parameters.RepositoryId))\n                                                    fileVersion\n                                                    (uploadMetadata.BlobUriWithSasToken)\n                                                    parameters.CorrelationId\n                                                with\n                                            | Ok result -> () //logToAnsiConsole Colors.Verbose $\"In Services.uploadFilesToObjectStorage(): Uploaded {fileVersion.GetObjectFileName} to object storage.\"\n                                            | Error error ->\n                                                logToAnsiConsole\n                                                    Colors.Error\n                                                    $\"Error uploading {fileVersion.GetObjectFileName} to object storage: {error.Error}\"\n\n                                                errors.Enqueue(error)\n                                        }\n                                    ))\n                            )\n\n                        if errors |> Seq.isEmpty then\n                            return Ok(GraceReturnValue.Create true parameters.CorrelationId)\n                        else\n                            // use Seq.fold to create a single error message from the ConcurrentQueue<GraceError>\n                            let errorMessage =\n                                errors\n                                |> Seq.fold (fun acc error -> $\"{acc}\\n{error.Error}\") \"\"\n\n                            let graceError = GraceError.Create (getErrorMessage StorageError.FailedUploadingFilesToObjectStorage) parameters.CorrelationId\n\n                            return Error graceError |> enhance \"Errors\" errorMessage\n                    | Error error -> return Error error\n                else\n                    return Ok(GraceReturnValue.Create true parameters.CorrelationId)\n            | AWSS3 -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n            | GoogleCloudStorage -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n        }\n\n    /// Creates an updated LocalDirectoryVersion instance, with a new DirectoryId, based on changes to an existing one.\n    ///\n    /// If this is a new subdirectory, the LocalDirectoryVersion will have just been created with empty subdirectory and file lists.\n    let processChangedDirectoryVersion (newGraceStatus: GraceStatus) (previousDirectoryVersion: LocalDirectoryVersion) =\n\n        // We process the deepest directories first, so we know any subdirectories of this one will already be in newGraceStatus.\n\n        // Get LocalDirectoryVersion instances for the subdirectories of this DirectoryVersion.\n        let subdirectoryVersions =\n            if previousDirectoryVersion.Directories.Count > 0 then\n                previousDirectoryVersion\n                    .Directories\n                    .Select(fun directoryId ->\n                        let localDirectoryVersion =\n                            newGraceStatus\n                                .Index\n                                .FirstOrDefault(\n                                    (fun kvp -> kvp.Key = directoryId),\n                                    KeyValuePair(Guid.Empty, LocalDirectoryVersion.Default)\n                                )\n                                .Value\n\n                        if localDirectoryVersion.DirectoryVersionId\n                           <> Guid.Empty then\n                            Some localDirectoryVersion\n                        else\n                            None)\n                    .Where(fun opt -> Option.isSome opt)\n                    .Select(fun opt -> Option.get opt)\n                    .ToList()\n            else\n                List<LocalDirectoryVersion>()\n\n        // Get the new SHA-256 hash for the updated contents of this directory.\n        let newSha256Hash = computeSha256ForDirectory (previousDirectoryVersion.RelativePath) subdirectoryVersions previousDirectoryVersion.Files\n\n        let directoryInfo = DirectoryInfo(Path.Combine(Current().RootDirectory, previousDirectoryVersion.RelativePath))\n\n        // Create a new LocalDirectoryVersion that contains a new Id and the updated SHA-256 hash.\n        let newDirectoryVersion =\n            LocalDirectoryVersion.Create\n                (Guid.NewGuid())\n                previousDirectoryVersion.OwnerId\n                previousDirectoryVersion.OrganizationId\n                previousDirectoryVersion.RepositoryId\n                previousDirectoryVersion.RelativePath\n                newSha256Hash\n                previousDirectoryVersion.Directories\n                previousDirectoryVersion.Files\n                (getLocalDirectorySize previousDirectoryVersion.Files)\n                directoryInfo.LastWriteTimeUtc\n\n        newDirectoryVersion\n\n    /// Determines if the given difference is for a directory, instead of a file.\n    let isDirectoryChange (difference: FileSystemDifference) =\n        match difference.FileSystemEntryType with\n        | FileSystemEntryType.Directory -> true\n        | FileSystemEntryType.File -> false\n\n    /// Determines if the given difference is for a file, instead of a directory.\n    let isFileChange difference = not <| isDirectoryChange difference\n\n    /// Processes directory additions or changes\n    let private processDirectoryChange (newGraceStatus: GraceStatus) (previousGraceStatus: GraceStatus) (difference: FileSystemDifference) =\n\n        let previousRootVersion = getRootDirectoryVersion previousGraceStatus\n\n        let previousDirectoryVersion =\n            previousGraceStatus\n                .Index\n                .FirstOrDefault(\n                    (fun kvp -> kvp.Value.RelativePath = difference.RelativePath),\n                    KeyValuePair(Guid.Empty, LocalDirectoryVersion.Default)\n                )\n                .Value\n\n        if previousDirectoryVersion.DirectoryVersionId\n           <> Guid.Empty then\n            Some(processChangedDirectoryVersion newGraceStatus previousDirectoryVersion)\n        else\n            let directoryInfo = DirectoryInfo(Path.Combine(Current().RootDirectory, difference.RelativePath))\n            let sha256Hash = computeSha256ForDirectory difference.RelativePath (List()) (List())\n\n            Some(\n                LocalDirectoryVersion.Create\n                    (Guid.NewGuid())\n                    previousRootVersion.OwnerId\n                    previousRootVersion.OrganizationId\n                    previousRootVersion.RepositoryId\n                    difference.RelativePath\n                    sha256Hash\n                    (List())\n                    (List())\n                    Constants.InitialDirectorySize\n                    directoryInfo.LastWriteTimeUtc\n            )\n\n    /// Processes file additions to a directory\n    let private processFileAddition\n        (changedDirectoryVersions: ConcurrentDictionary<RelativePath, LocalDirectoryVersion>)\n        (newGraceStatus: GraceStatus)\n        (difference: FileSystemDifference)\n        =\n        task {\n\n            let fileInfo = FileInfo(Path.Combine(Current().RootDirectory, difference.RelativePath))\n            let relativeDirectoryPath = getLocalRelativeDirectory fileInfo.DirectoryName (Current().RootDirectory)\n\n            let directoryVersion =\n                let mutable changedDirectoryVersion = LocalDirectoryVersion.Default\n\n                if changedDirectoryVersions.TryGetValue(relativeDirectoryPath, &changedDirectoryVersion) then\n                    changedDirectoryVersion\n                else\n                    newGraceStatus.Index.Values.FirstOrDefault((fun dv -> dv.RelativePath = relativeDirectoryPath), LocalDirectoryVersion.Default)\n\n            match! createLocalFileVersion fileInfo with\n            | Some fileVersion ->\n                directoryVersion.Files.Add(fileVersion)\n                let updated = { directoryVersion with Size = directoryVersion.Files.Sum(fun file -> int64 (file.Size)) }\n\n                changedDirectoryVersions.AddOrUpdate(directoryVersion.RelativePath, (fun _ -> updated), (fun _ _ -> updated))\n                |> ignore\n            | None -> ()\n        }\n\n    /// Processes file changes (updates)\n    let private processFileChange\n        (changedDirectoryVersions: ConcurrentDictionary<RelativePath, LocalDirectoryVersion>)\n        (newGraceStatus: GraceStatus)\n        (difference: FileSystemDifference)\n        =\n        task {\n\n            let fileInfo = FileInfo(Path.Combine(Current().RootDirectory, difference.RelativePath))\n            let relativeDirectoryPath = normalizeFilePath (getLocalRelativeDirectory fileInfo.DirectoryName (Current().RootDirectory))\n\n            let directoryVersion =\n                let alreadyChanged =\n                    changedDirectoryVersions.Values.FirstOrDefault((fun dv -> dv.RelativePath = relativeDirectoryPath), LocalDirectoryVersion.Default)\n\n                if alreadyChanged.DirectoryVersionId <> Guid.Empty then\n                    alreadyChanged\n                else\n                    newGraceStatus.Index.Values.First(fun dv -> dv.RelativePath = relativeDirectoryPath)\n\n            let existingFileIndex = directoryVersion.Files.FindIndex(fun file -> file.RelativePath = difference.RelativePath)\n            let existingFileVersion = directoryVersion.Files[existingFileIndex]\n\n            match! createLocalFileVersion fileInfo with\n            | Some fileVersion ->\n                if fileVersion.Sha256Hash\n                   <> existingFileVersion.Sha256Hash then\n                    directoryVersion.Files.RemoveAt(existingFileIndex)\n                    directoryVersion.Files.Add(fileVersion)\n                    let updated = { directoryVersion with Size = directoryVersion.Files.Sum(fun file -> int64 (file.Size)) }\n\n                    changedDirectoryVersions.AddOrUpdate(directoryVersion.RelativePath, (fun _ -> updated), (fun _ _ -> updated))\n                    |> ignore\n            | None -> ()\n        }\n\n    /// Processes file deletions\n    let private processFileDeletion\n        (changedDirectoryVersions: ConcurrentDictionary<RelativePath, LocalDirectoryVersion>)\n        (newGraceStatus: GraceStatus)\n        (difference: FileSystemDifference)\n        =\n\n        let fileInfo = FileInfo(Path.Combine(Current().RootDirectory, difference.RelativePath))\n        let relativeDirectoryPath = getLocalRelativeDirectory fileInfo.DirectoryName (Current().RootDirectory)\n\n        let directoryVersion =\n            newGraceStatus\n                .Index\n                .Values\n                .Where(fun dv -> dv.RelativePath = relativeDirectoryPath)\n                .ToList()\n\n        match directoryVersion.Count with\n        | 0 -> ()\n        | 1 ->\n            let dv = directoryVersion[0]\n            let index = dv.Files.FindIndex(fun file -> file.RelativePath = difference.RelativePath)\n            dv.Files.RemoveAt(index)\n            let updated = { dv with Size = dv.Files.Sum(fun file -> int64 (file.Size)) }\n\n            changedDirectoryVersions.AddOrUpdate(dv.RelativePath, (fun _ -> updated), (fun _ _ -> updated))\n            |> ignore\n        | _ -> ()\n\n    /// Recursively processes changed directories from leaf to root\n    let rec private processChangedDirectoriesBottomUp\n        (newGraceStatus: GraceStatus)\n        (changedDirectoryVersions: ConcurrentDictionary<RelativePath, LocalDirectoryVersion>)\n        (newDirectoryVersions: List<LocalDirectoryVersion>)\n        =\n\n        if changedDirectoryVersions.IsEmpty then\n            ()\n        else\n            let relativePath =\n                changedDirectoryVersions\n                    .Keys\n                    .OrderByDescending(fun rp -> countSegments rp)\n                    .First()\n\n            let mutable previousDirectoryVersion = LocalDirectoryVersion.Default\n\n            if changedDirectoryVersions.TryRemove(relativePath, &previousDirectoryVersion) then\n                let newDirectoryVersion = processChangedDirectoryVersion newGraceStatus previousDirectoryVersion\n                newDirectoryVersions.Add(newDirectoryVersion)\n\n                let mutable previous = LocalDirectoryVersion.Default\n                let foundPrevious = newGraceStatus.Index.TryRemove(previousDirectoryVersion.DirectoryVersionId, &previous)\n\n                newGraceStatus.Index.TryAdd(newDirectoryVersion.DirectoryVersionId, newDirectoryVersion)\n                |> ignore\n\n                match getParentPath relativePath with\n                | Some path ->\n                    let dv =\n                        let alreadyChanged = changedDirectoryVersions.Values.FirstOrDefault((fun dv -> dv.RelativePath = path), LocalDirectoryVersion.Default)\n\n                        if alreadyChanged.DirectoryVersionId <> Guid.Empty then\n                            alreadyChanged\n                        else\n                            newGraceStatus.Index.Values.First(fun dv -> dv.RelativePath = path)\n\n                    if foundPrevious then\n                        dv.Directories.Remove(previous.DirectoryVersionId)\n                        |> ignore\n\n                    dv.Directories.Add(newDirectoryVersion.DirectoryVersionId)\n                    |> ignore\n\n                    changedDirectoryVersions.AddOrUpdate(path, (fun _ -> dv), (fun _ _ -> dv))\n                    |> ignore\n\n                    // Recursively process the parent\n                    processChangedDirectoriesBottomUp newGraceStatus changedDirectoryVersions newDirectoryVersions\n                | None -> ()\n\n    /// Main refactored function\n    let getNewGraceStatusAndDirectoryVersions (previousGraceStatus: GraceStatus) (differences: IEnumerable<FileSystemDifference>) =\n        task {\n            if parseResult |> isOutputFormat \"Verbose\" then\n                logToAnsiConsole Colors.Verbose $\"In getNewGraceStatusAndDirectoryVersions: differences:{Environment.NewLine}{serialize differences}\"\n\n            let changedDirectoryVersions = ConcurrentDictionary<RelativePath, LocalDirectoryVersion>()\n            let mutable newGraceStatus = { previousGraceStatus with Index = GraceIndex(previousGraceStatus.Index) }\n\n            // Process directory changes (Add/Change/Delete)\n            for difference in differences.Where(fun d -> isDirectoryChange d) do\n                match difference.DifferenceType with\n                | Add\n                | Change ->\n                    match processDirectoryChange newGraceStatus previousGraceStatus difference with\n                    | Some newDirectoryVersion ->\n                        let previousDirectoryVersions = newGraceStatus.Index.Values.Where(fun dv -> dv.RelativePath = difference.RelativePath)\n\n                        logToAnsiConsole\n                            Colors.Verbose\n                            $\"Processing directory {difference.DifferenceType} for path: {difference.RelativePath}. Previous versions: {serialize previousDirectoryVersions}.\"\n\n                        let mutable previous = LocalDirectoryVersion.Default\n                        // Remove the previous directory version from the index.\n                        for previousDirectoryVersion in previousDirectoryVersions do\n                            newGraceStatus.Index.TryRemove(previousDirectoryVersion.DirectoryVersionId, &previous)\n                            |> ignore\n\n                        // Add the new directory version to the index.\n                        newGraceStatus.Index.AddOrUpdate(\n                            newDirectoryVersion.DirectoryVersionId,\n                            (fun _ -> newDirectoryVersion),\n                            (fun _ _ -> newDirectoryVersion)\n                        )\n                        |> ignore\n\n                        if difference.DifferenceType = Add then\n                            changedDirectoryVersions.AddOrUpdate(difference.RelativePath, (fun _ -> newDirectoryVersion), (fun _ _ -> newDirectoryVersion))\n                            |> ignore\n                    | None -> ()\n                | Delete ->\n                    let mutable directoryVersion = newGraceStatus.Index.Values.First(fun dv -> dv.RelativePath = difference.RelativePath)\n\n                    newGraceStatus.Index.TryRemove(directoryVersion.DirectoryVersionId, &directoryVersion)\n                    |> ignore\n\n            // Process file changes (Add/Change/Delete)\n            for difference in differences.Where(fun d -> isFileChange d) do\n                match difference.DifferenceType with\n                | Add -> do! processFileAddition changedDirectoryVersions newGraceStatus difference\n                | Change -> do! processFileChange changedDirectoryVersions newGraceStatus difference\n                | Delete -> processFileDeletion changedDirectoryVersions newGraceStatus difference\n\n            // Recursively process changed directories from leaf to root\n            let newDirectoryVersions = List<LocalDirectoryVersion>()\n            processChangedDirectoriesBottomUp newGraceStatus changedDirectoryVersions newDirectoryVersions\n\n            if newDirectoryVersions.Count > 0 then\n                let rootExists =\n                    newGraceStatus\n                        .Index\n                        .Values\n                        .FirstOrDefault(\n                            (fun dv -> dv.RelativePath = Constants.RootDirectoryPath),\n                            LocalDirectoryVersion.Default\n                        )\n                        .DirectoryVersionId\n                    <> Guid.Empty\n\n                if rootExists then\n                    let newRootDirectoryVersion = getRootDirectoryVersion newGraceStatus\n\n                    newGraceStatus <-\n                        { newGraceStatus with\n                            RootDirectoryId = newRootDirectoryVersion.DirectoryVersionId\n                            RootDirectorySha256Hash = newRootDirectoryVersion.Sha256Hash\n                        }\n\n                return (newGraceStatus, newDirectoryVersions)\n            else\n                return (previousGraceStatus, newDirectoryVersions)\n        }\n\n    /// Ensures that the provided directory versions are uploaded to Grace Server.\n    /// This will add new directory versions, and ignore existing directory versions, as they are immutable.\n    let uploadDirectoryVersions (localDirectoryVersions: List<LocalDirectoryVersion>) correlationId =\n        let directoryVersions =\n            localDirectoryVersions\n                .Select(fun ldv -> ldv.ToDirectoryVersion)\n                .ToList()\n\n        let saveParameters = SaveDirectoryVersionsParameters()\n        saveParameters.OwnerId <- $\"{Current().OwnerId}\"\n        saveParameters.OwnerName <- Current().OwnerName\n        saveParameters.OrganizationId <- $\"{Current().OrganizationId}\"\n        saveParameters.OrganizationName <- Current().OrganizationName\n        saveParameters.RepositoryId <- $\"{Current().RepositoryId}\"\n        saveParameters.RepositoryName <- Current().RepositoryName\n        saveParameters.CorrelationId <- correlationId\n        saveParameters.DirectoryVersions <- directoryVersions\n\n        DirectoryVersion.SaveDirectoryVersions saveParameters\n\n    /// The full path of the inter-process communication file that grace watch uses to communicate with other invocations of Grace.\n    let IpcFileName () = Path.Combine(Path.GetTempPath(), \"Grace\", Current().BranchName, Constants.IpcFileName)\n\n    /// Updates the contents of the `grace watch` status inter-process communication file.\n    let updateGraceWatchInterprocessFile\n        (graceStatus: GraceStatus)\n        (directoryIdsOverride: HashSet<DirectoryVersionId> option)\n        =\n        task {\n            try\n                let directoryIds =\n                    match directoryIdsOverride with\n                    | Some ids -> HashSet<DirectoryVersionId>(ids)\n                    | None -> HashSet<DirectoryVersionId>(graceStatus.Index.Keys)\n\n                let newGraceWatchStatus =\n                    {\n                        UpdatedAt = getCurrentInstant ()\n                        RootDirectoryId = graceStatus.RootDirectoryId\n                        RootDirectorySha256Hash = graceStatus.RootDirectorySha256Hash\n                        LastFileUploadInstant = graceStatus.LastSuccessfulFileUpload\n                        LastDirectoryVersionInstant = graceStatus.LastSuccessfulDirectoryVersionUpload\n                        DirectoryIds = directoryIds\n                    }\n                //logToAnsiConsole Colors.Important $\"In updateGraceWatchStatus. newGraceWatchStatus.UpdatedAt: {newGraceWatchStatus.UpdatedAt.ToString(InstantPattern.ExtendedIso.PatternText, CultureInfo.InvariantCulture)}.\"\n                //logToAnsiConsole Colors.Highlighted $\"{Markup.Escape(EnhancedStackTrace.Current().ToString())}\"\n\n                Directory.CreateDirectory(Path.GetDirectoryName(IpcFileName()))\n                |> ignore\n\n                use fileStream = new FileStream(IpcFileName(), FileMode.Create, FileAccess.Write, FileShare.None)\n\n                do! serializeAsync fileStream newGraceWatchStatus\n                graceWatchStatusUpdateTime <- newGraceWatchStatus.UpdatedAt\n                logToAnsiConsole Colors.Important $\"Wrote inter-process communication file.\"\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"Exception in updateGraceWatchInterprocessFile.\"\n                logToAnsiConsole Colors.Error $\"ex.GetType: {ex.GetType().FullName}{Environment.NewLine}{Environment.NewLine}\"\n                logToAnsiConsole Colors.Error $\"ex.Message: {ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}\"\n        }\n\n    /// Reads the `grace watch` status inter-process communication file.\n    let getGraceWatchStatus () =\n        task {\n            try\n                // If the file exists, `grace watch` is running.\n                if File.Exists(IpcFileName()) then\n                    //logToAnsiConsole Colors.Verbose $\"File {IpcFileName} exists.\"\n                    use fileStream = new FileStream(IpcFileName(), FileMode.Open, FileAccess.Read, FileShare.Read)\n\n                    let! graceWatchStatus = deserializeAsync<GraceWatchStatus> fileStream\n\n                    // `grace watch` updates the file at least every five minutes to indicate that it's still alive.\n                    // When `grace watch` exits, the status file is deleted in a try...finally (Program.CLI.fs), so the only\n                    //   circumstance where it would be on-disk without `grace watch` running is if the process were killed.\n                    // Just to be safe, we're going to check that the file has been written in the last five minutes.\n                    if\n                        graceWatchStatus.UpdatedAt\n                        > getCurrentInstant().Minus(Duration.FromMinutes(5.0))\n                    then\n                        return Some graceWatchStatus\n                    else\n                        return None // File is more than five minutes old, so something weird happened and we shouldn't trust the information.\n                else\n                    //logToAnsiConsole Colors.Verbose $\"File {IpcFileName} does not exist.\"\n                    return None // `grace watch` isn't running.\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"Exception when reading inter-process communication file.\"\n                logToAnsiConsole Colors.Error $\"ex.GetType: {ex.GetType().FullName}.\"\n                logToAnsiConsole Colors.Error $\"ex.Message: {StringExtensions.EscapeMarkup(ex.Message)}.\"\n                logToAnsiConsole Colors.Error $\"{Environment.NewLine}{StringExtensions.EscapeMarkup(ex.StackTrace)}.\"\n                return None\n        }\n\n    /// Checks if a file already exists in the object cache.\n    let isFileInObjectCache (fileVersion: LocalFileVersion) =\n        task {\n            let objectFileName = fileVersion.GetObjectFileName\n\n            let objectFilePath = Path.Combine(Current().ObjectDirectory, fileVersion.RelativeDirectory, objectFileName)\n\n            return File.Exists(objectFilePath)\n        }\n\n    /// Updates the Grace Status index with new directory versions after getting them from the server.\n    let updateGraceStatusWithNewDirectoryVersionsFromServer\n        (graceStatus: GraceStatus)\n        (newDirectoryVersionDtos: IEnumerable<Grace.Types.DirectoryVersion.DirectoryVersionDto>)\n        =\n        let newGraceIndex = GraceIndex(graceStatus.Index)\n        let mutable dvForDeletions = LocalDirectoryVersion.Default\n\n        if parseResult |> isOutputFormat \"Verbose\" then\n            logToAnsiConsole\n                Colors.Verbose\n                $\"In updateGraceStatusWithNewDirectoryVersionsFromServer: Processing {newDirectoryVersionDtos.Count()} new DirectoryVersions.\"\n\n        // First, either add the new ones, or replace the existing ones.\n        for newDirectoryVersionDto in newDirectoryVersionDtos do\n            let newDirectoryVersion = newDirectoryVersionDto.DirectoryVersion\n\n            logToAnsiConsole Colors.Verbose $\"Processing new DirectoryVersion: {newDirectoryVersion.RelativePath}.\"\n\n            let existingDirectoryVersion =\n                newGraceIndex.Values.FirstOrDefault((fun dv -> dv.RelativePath = newDirectoryVersion.RelativePath), LocalDirectoryVersion.Default)\n\n            if existingDirectoryVersion.DirectoryVersionId\n               <> LocalDirectoryVersion.Default.DirectoryVersionId then\n                // We already have an entry with the same RelativePath, so remove the old one and add the new one.\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole Colors.Verbose $\"Replacing existing DirectoryVersion for path: {newDirectoryVersion.RelativePath}.\"\n\n                newGraceIndex.TryRemove(existingDirectoryVersion.DirectoryVersionId, &dvForDeletions)\n                |> ignore\n\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole Colors.Verbose $\"Removed old DirectoryVersion for path: {newDirectoryVersion.RelativePath}.\"\n\n                newGraceIndex.AddOrUpdate(\n                    newDirectoryVersion.DirectoryVersionId,\n                    (fun _ -> newDirectoryVersion.ToLocalDirectoryVersion DateTime.UtcNow),\n                    (fun _ _ -> newDirectoryVersion.ToLocalDirectoryVersion DateTime.UtcNow)\n                )\n                |> ignore\n\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole Colors.Verbose $\"Added new DirectoryVersion for path: {newDirectoryVersion.RelativePath}.\"\n            else\n                // We didn't find the RelativePath, so it's a new DirectoryVersion.\n                newGraceIndex.AddOrUpdate(\n                    newDirectoryVersion.DirectoryVersionId,\n                    (fun _ -> newDirectoryVersion.ToLocalDirectoryVersion DateTime.UtcNow),\n                    (fun _ _ -> newDirectoryVersion.ToLocalDirectoryVersion DateTime.UtcNow)\n                )\n                |> ignore\n\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole Colors.Verbose $\"Added new DirectoryVersion for path: {newDirectoryVersion.RelativePath}.\"\n\n        // Finally, delete any that don't exist anymore.\n        // Get the list of the all of the subdirectories referenced in the DirectoryVersions left after replacing existing ones and adding new ones.\n        let allSubdirectories =\n            HashSet<DirectoryVersionId>(\n                newGraceIndex.Values.Select(fun dv -> dv.Directories)\n                |> Seq.collect (fun dvs -> dvs)\n            )\n\n        let allSubdirectoriesDistinct = allSubdirectories |> Seq.distinct\n        // Now check every DirectoryVersion in newGraceIndex to see if it's in the list of subdirectories.\n        //   If it's no longer referenced by anyone, that means we can delete it.\n        for directoryVersion in newGraceIndex.Values do\n            if not\n               <| (directoryVersion.RelativePath = Constants.RootDirectoryPath)\n               && not\n                  <| allSubdirectoriesDistinct.Contains(directoryVersion.DirectoryVersionId) then\n                if parseResult |> isOutputFormat \"Verbose\" then\n                    logToAnsiConsole\n                        Colors.Verbose\n                        $\"Removing DirectoryVersion for path: {directoryVersion.RelativePath}; DirectoryVersionId: {directoryVersion.DirectoryVersionId}.\"\n\n                newGraceIndex.TryRemove(directoryVersion.DirectoryVersionId, &dvForDeletions)\n                |> ignore\n\n        let rootDirectoryVersion = newGraceIndex.Values.First(fun dv -> dv.RelativePath = Constants.RootDirectoryPath)\n\n        let newGraceStatus =\n            {\n                Index = newGraceIndex\n                RootDirectoryId = rootDirectoryVersion.DirectoryVersionId\n                RootDirectorySha256Hash = rootDirectoryVersion.Sha256Hash\n                LastSuccessfulDirectoryVersionUpload = graceStatus.LastSuccessfulDirectoryVersionUpload\n                LastSuccessfulFileUpload = graceStatus.LastSuccessfulFileUpload\n            }\n\n        if parseResult |> isOutputFormat \"Verbose\" then\n            logToAnsiConsole\n                Colors.Verbose\n                $\"In updateGraceStatusWithNewDirectoryVersionsFromServer: Returning new GraceStatus with {newGraceStatus.Index.Count} DirectoryVersions.\"\n\n        newGraceStatus\n\n    /// Gets the file name used to indicate to `grace watch` that updates are in progress from another Grace command, and that it should ignore them.\n    let updateInProgressFileName () =\n        let directory = Path.Combine(Path.GetTempPath(), \"Grace\", Current().BranchName)\n        Directory.CreateDirectory(directory) |> ignore\n\n        getNativeFilePath (Path.Combine(Path.GetTempPath(), \"Grace\", Current().BranchName, Constants.UpdateInProgressFileName))\n\n    /// Updates the working directory to match the contents of new DirectoryVersions.\n    ///\n    /// In general, this means copying new and changed files into place, and removing deleted files and directories.\n    let updateWorkingDirectory\n        (previousGraceStatus: GraceStatus)\n        (updatedGraceStatus: GraceStatus)\n        (newDirectoryVersionDtos: IEnumerable<Grace.Types.DirectoryVersion.DirectoryVersionDto>)\n        (correlationId: CorrelationId)\n        =\n        task {\n            // Loop through each new DirectoryVersion.\n            for newDirectoryVersionDto in newDirectoryVersionDtos do\n                let newDirectoryVersion = newDirectoryVersionDto.DirectoryVersion\n                // Get the previous DirectoryVersion, so we can compare contents below.\n                let previousDirectoryVersion =\n                    previousGraceStatus.Index.Values.FirstOrDefault(\n                        (fun dv -> dv.RelativePath = newDirectoryVersion.RelativePath),\n                        LocalDirectoryVersion.Default\n                    )\n                // Ensure that the directory exists on disk.\n                let directoryInfo = Directory.CreateDirectory(newDirectoryVersion.FullName)\n\n                // Copy new and existing files into place.\n                let newLocalFileVersions = newDirectoryVersion.Files.Select(fun file -> file.ToLocalFileVersion DateTime.UtcNow)\n\n                for fileVersion in newLocalFileVersions do\n                    let existingFileOnDisk = FileInfo(fileVersion.FullName)\n                    let objectFile = FileInfo(fileVersion.FullObjectPath)\n\n                    if not <| objectFile.Exists then\n                        // This is an error. There _should_ be a file in the object cache for every file in each DirectoryVersion.\n                        //   Anyway, we'll just download it from the server (again).\n                        let getDownloadUriParameters =\n                            Storage.GetDownloadUriParameters(\n                                OwnerId = $\"{Current().OwnerId}\",\n                                OwnerName = Current().OwnerName,\n                                OrganizationId = $\"{Current().OrganizationId}\",\n                                OrganizationName = Current().OrganizationName,\n                                RepositoryId = $\"{Current().RepositoryId}\",\n                                RepositoryName = Current().RepositoryName,\n                                CorrelationId = correlationId\n                            )\n\n                        match! downloadFilesFromObjectStorage getDownloadUriParameters [| fileVersion |] correlationId with\n                        | Ok _ ->\n                            logToAnsiConsole Colors.Verbose $\"Downloaded {fileVersion.FullObjectPath} from the object storage provider.\"\n\n                            ()\n                        | Error error ->\n                            logToAnsiConsole\n                                Colors.Error\n                                $\"An error occurred while downloading a file from the object storage provider. CorrelationId: {correlationId}.\"\n\n                            logToAnsiConsole Colors.Error $\"{error}\"\n\n                    if existingFileOnDisk.Exists then\n                        // Need to compare existing file to new version from the object cache.\n                        let findFileVersionFromPreviousGraceStatus = previousDirectoryVersion.Files.Where(fun f -> f.RelativePath = fileVersion.RelativePath)\n\n                        if findFileVersionFromPreviousGraceStatus.Count() > 0 then\n                            let fileVersionFromPreviousGraceStatus = findFileVersionFromPreviousGraceStatus.First()\n                            // If the length is different, or the Sha256Hash is changing in the new version, we'll delete the\n                            //   file in the working directory, and copy the version from the object cache to replace it.\n                            if existingFileOnDisk.Length <> fileVersion.Size\n                               || fileVersionFromPreviousGraceStatus.Sha256Hash\n                                  <> fileVersion.Sha256Hash then\n                                //logToAnsiConsole\n                                //    Colors.Verbose\n                                //    $\"Replacing {fileVersion.FullName}; previous length: {fileVersionFromPreviousGraceStatus.Size}; new length: {fileVersion.Size}.\"\n\n                                existingFileOnDisk.Delete()\n                                File.Copy(fileVersion.FullObjectPath, fileVersion.FullName)\n                    else\n                        // No existing file, so just copy it into place.\n                        //logToAnsiConsole Colors.Verbose $\"Copying file {fileVersion.FullName} from object cache; no existing file.\"\n                        File.Copy(fileVersion.FullObjectPath, fileVersion.FullName)\n\n                // Delete unnecessary directories.\n                // Get DirectoryVersions for the subdirectories of the new DirectoryVersion.\n                //logToAnsiConsole\n                //    Colors.Verbose\n                //    $\"Services.CLI.fs: updateWorkingDirectory(): {Markup.Escape(serialize (updatedGraceStatus.Index.Select(fun x -> x.Value.DirectoryVersionId)))}\"\n\n                //logToAnsiConsole\n                //    Colors.Verbose\n                //    $\"Services.CLI.fs: updateWorkingDirectory(): {Markup.Escape(serialize (newDirectoryVersions.Select(fun x -> x.DirectoryVersionId)))}\"\n\n                //let previousDirectoryIds = previousGraceStatus.Index.Values.Select(fun dv -> (dv.DirectoryVersionId, dv.RelativePath))\n                //let updatedDirectoryIds = updatedGraceStatus.Index.Values.Select(fun dv -> (dv.DirectoryVersionId, dv.RelativePath))\n                //logToAnsiConsole Colors.Verbose $\"{serialize previousDirectoryIds}\"\n                //logToAnsiConsole Colors.Verbose $\"{serialize updatedDirectoryIds}\"\n\n                let subdirectoryVersions = newDirectoryVersion.Directories.Select(fun directoryVersionId -> updatedGraceStatus.Index[directoryVersionId])\n                // Loop through the actual subdirectories on disk.\n                for subdirectoryInfo in directoryInfo.EnumerateDirectories().ToArray() do\n                    // If we don't have this subdirectory listed in new parent DirectoryVersion, and it's a directory that we shouldn't ignore,\n                    //    that means that it was deleted, and we should delete it from the working directory.\n                    let relativeSubdirectoryPath = Path.GetRelativePath(Current().RootDirectory, subdirectoryInfo.FullName)\n\n                    if not\n                       <| (subdirectoryVersions\n                           |> Seq.exists (fun subdirectoryVersion -> subdirectoryVersion.RelativePath = relativeSubdirectoryPath))\n                       && shouldNotIgnoreDirectory subdirectoryInfo.FullName then\n                        //logToAnsiConsole Colors.Verbose $\"Deleting directory {subdirectoryInfo.FullName}.\"\n                        subdirectoryInfo.Delete(true)\n\n                // Delete unnecessary files.\n                // Loop through the actual files on disk.\n                for fileInfo in directoryInfo.EnumerateFiles() do\n                    // If we don't have this file in the new version of the directory, and it's a file that we shouldn't ignore,\n                    //   that means that it was deleted, and we should delete it from the working directory.\n                    // Ignored files get... ignored.\n                    if not\n                       <| newLocalFileVersions.Any(fun fileVersion -> fileVersion.FullName = fileInfo.FullName)\n                       && not <| shouldIgnoreFile fileInfo.FullName then\n                        //logToAnsiConsole Colors.Verbose $\"Deleting file {fileInfo.FullName}.\"\n                        fileInfo.Delete()\n        }\n\n    /// Creates a save reference with the given message.\n    let createSaveReference rootDirectoryVersion message correlationId =\n        task {\n            //Activity.Current <- new Activity(\"createSaveReference\")\n            let createReferenceParameters =\n                Parameters.Branch.CreateReferenceParameters(\n                    OwnerId = $\"{Current().OwnerId}\",\n                    OrganizationId = $\"{Current().OrganizationId}\",\n                    RepositoryId = $\"{Current().RepositoryId}\",\n                    BranchId = $\"{Current().BranchId}\",\n                    CorrelationId = correlationId,\n                    DirectoryVersionId = rootDirectoryVersion.DirectoryVersionId,\n                    Sha256Hash = rootDirectoryVersion.Sha256Hash,\n                    Message = message\n                )\n\n            let! result = Branch.Save createReferenceParameters\n            //Activity.Current.SetTag(\"CorrelationId\", correlationId) |> ignore\n            match result with\n            | Ok returnValue ->\n                //logToAnsiConsole Colors.Verbose $\"Created a save in branch {Current().BranchName}. Sha256Hash: {rootDirectoryVersion.Sha256Hash.Substring(0, 8)}. CorrelationId: {returnValue.CorrelationId}.\"\n                //Activity.Current.AddTag(\"Created Save reference\", \"true\")\n                //                .SetStatus(ActivityStatusCode.Ok, returnValue.ReturnValue) |> ignore\n                ()\n            | Error error ->\n                logToAnsiConsole\n                    Colors.Error\n                    $\"An error occurred while creating a save that contains the current differences. CorrelationId: {error.CorrelationId}.\"\n            //Activity.Current.AddTag(\"Created Save reference\", \"false\")\n            //                .AddTag(\"Server path\", error.Properties[\"Path\"])\n            //                .SetStatus(ActivityStatusCode.Error, error.Error) |> ignore\n            //Activity.Current.Dispose()\n            return result\n        }\n\n    /// Generates a temporary file name within the ObjectDirectory, and returns the full file path.\n    /// This file name will be used to copy modified files into before renaming them with their proper names and SHA256 values.\n    let getTemporaryFilePath () =\n        let tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), \"Grace\", Current().BranchName))\n\n        Path.GetFullPath(Path.Combine(tempDirectory.FullName, $\"{Path.GetRandomFileName()}.gracetmp\"))\n\n    /// Copies a file to the Object Directory, and returns a new FileVersion. The SHA-256 hash is computed and included in the object file name.\n    let copyToObjectDirectory (filePath: FilePath) : Task<FileVersion option> =\n        task {\n            try\n                if File.Exists(filePath) then\n                    // First, capture the file by copying it to a temp name\n                    let tempFilePath = getTemporaryFilePath ()\n                    //logToConsole $\"filePath: {filePath}; tempFilePath: {tempFilePath}\"\n                    let mutable iteration = 0\n\n                    Constants.DefaultFileCopyRetryPolicy.Execute (fun () ->\n                        //iteration <- iteration + 1\n                        //logToAnsiConsole Colors.Deemphasized $\"Attempt #{iteration} to copy file to object directory...\"\n                        File.Copy(sourceFileName = filePath, destFileName = tempFilePath, overwrite = true))\n\n                    // Now that we've copied it, compute the SHA-256 hash.\n                    let relativeFilePath = Path.GetRelativePath(Current().RootDirectory, filePath)\n\n                    use tempFileStream = File.Open(tempFilePath, fileStreamOptionsRead)\n\n                    let! isBinary = isBinaryFile tempFileStream\n                    tempFileStream.Position <- 0\n                    let! sha256Hash = computeSha256ForFile tempFileStream relativeFilePath\n                    //logToConsole $\"filePath: {filePath}; tempFilePath: {tempFilePath}; SHA256: {sha256Hash}\"\n\n                    // I'm going to rename the temp file below, using the SHA-256 hash, so I'll close the file and dispose the stream first.\n                    do! tempFileStream.DisposeAsync()\n\n                    // Get the new name for this version of the file, including the SHA-256 hash.\n                    let relativeDirectoryPath = getLocalRelativeDirectory filePath (Current().RootDirectory)\n\n                    let objectFileName = getObjectFileName filePath sha256Hash\n\n                    let objectDirectoryPath = Path.Combine(Current().ObjectDirectory, relativeDirectoryPath)\n\n                    let objectFilePath = Path.Combine(objectDirectoryPath, objectFileName)\n                    //logToConsole $\"relativeDirectoryPath: {relativeDirectoryPath}; objectFileName: {objectFileName}; objectFilePath: {objectFilePath}\"\n\n                    // If we don't already have this file, with this exact SHA256, make sure the directory exists,\n                    //   and rename the temp file to the proper SHA256-enhanced name of the file.\n                    if not (File.Exists(objectFilePath)) then\n                        //logToConsole $\"Before moving temp file to object storage...\"\n                        Directory.CreateDirectory(objectDirectoryPath)\n                        |> ignore // No-op if the directory already exists\n\n                        File.Move(tempFilePath, objectFilePath)\n                        //logToConsole $\"After moving temp file to object storage...\"\n                        let objectFilePathInfo = FileInfo(objectFilePath)\n                        //logToConsole $\"After creating FileInfo; Exists: {objectFilePathInfo.Exists}; FullName = {objectFilePathInfo.FullName}...\"\n                        //logToConsole $\"Finished copyToObjectDirectory for {filePath}; isBinary: {isBinary}; moved temp file to object directory.\"\n                        let relativePath = Path.GetRelativePath(Current().RootDirectory, filePath)\n\n                        return Some(FileVersion.Create (RelativePath relativePath) (Sha256Hash $\"{sha256Hash}\") (\"\") isBinary (objectFilePathInfo.Length))\n                    else\n                        // If we do already have this exact version of the file, just delete the temp file.\n                        File.Delete(tempFilePath)\n                        //logToConsole $\"Finished copyToObjectDirectory for {filePath}; object file already exists; deleted temp file.\"\n                        return None\n                //return result\n                else\n                    logToAnsiConsole Colors.Error $\"File {filePath} does not exist.\"\n                    return None\n            with\n            | ex ->\n                logToAnsiConsole Colors.Error $\"Exception in copyToObjectDirectory: {ExceptionResponse.Create ex}\"\n                return None\n        }\n\n    /// Copies new and updated files found in a list of FileSystemDifferences to the object directory.\n    let copyUpdatedFilesToObjectCache (t: ProgressTask) (differences: List<FileSystemDifference>) =\n        task {\n            // Get the list of files that have been added or changed.\n            let relativePathsOfUpdatedFiles =\n                differences\n                    .Select(fun difference ->\n                        match difference.DifferenceType with\n                        | Add ->\n                            match difference.FileSystemEntryType with\n                            | FileSystemEntryType.File -> Some difference.RelativePath\n                            | FileSystemEntryType.Directory -> None\n                        | Change ->\n                            match difference.FileSystemEntryType with\n                            | FileSystemEntryType.File -> Some difference.RelativePath\n                            | FileSystemEntryType.Directory -> None\n                        | Delete -> None)\n                    .Where(fun relativePathOption -> relativePathOption.IsSome)\n                    .Select(fun relativePath -> relativePath.Value)\n            //logToAnsiConsole Colors.Verbose $\"relativePathsOfUpdatedFiles: {serialize relativePathsOfUpdatedFiles}\"\n\n            // Create new LocalFileVersion instances for each updated file.\n            let increment =\n                if differences.Count > 0 then\n                    (100.0 - t.Value) / float differences.Count\n                else\n                    0.0\n\n            let newFileVersions = ConcurrentQueue<LocalFileVersion>()\n\n            do!\n                Parallel.ForEachAsync(\n                    relativePathsOfUpdatedFiles,\n                    Constants.ParallelOptions,\n                    (fun relativePath continuationToken ->\n                        ValueTask(\n                            task {\n                                //logToAnsiConsole Colors.Verbose $\"In Services.CLI.copyToObjectDirectory: Copying {relativePath} to object storage.\"\n                                match! copyToObjectDirectory (Path.Combine(Current().RootDirectory, relativePath)) with\n                                | Some fileVersion -> newFileVersions.Enqueue(fileVersion.ToLocalFileVersion(DateTime.UtcNow))\n                                //logToAnsiConsole Colors.Verbose $\"Copied {fileVersion.RelativePath} to {fileVersion.GetObjectFileName} in object storage.\"\n                                | None ->\n                                    logToAnsiConsole Colors.Error $\"Failed to copy {relativePath} to object storage.\"\n                                    ()\n\n                                t.Increment(increment)\n                            }\n                        ))\n                )\n\n            return newFileVersions\n        }\n\n    let private matchesOptionName (option: Option) (optionName: string) =\n        let normalizedName = optionName.TrimStart('-')\n\n        let hasAlias alias =\n            option.Aliases\n            |> Seq.exists (fun optionAlias -> optionAlias = alias)\n\n        option.Name = optionName\n        || option.Name = normalizedName\n        || hasAlias optionName\n        || hasAlias normalizedName\n\n    let rec private hasOptionInCommandResult (commandResult: CommandResult) (optionName: string) =\n        if isNull commandResult then\n            false\n        elif commandResult.Command.Options\n             |> Seq.exists (fun option -> matchesOptionName option optionName) then\n            true\n        else\n            match commandResult.Parent with\n            | :? CommandResult as parent -> hasOptionInCommandResult parent optionName\n            | _ -> false\n\n    /// Checks if an option was present in the definition of the command.\n    let isOptionPresent (parseResult: ParseResult) (optionName: string) =\n        if not <| isNull (parseResult.GetResult(optionName)) then\n            true\n        else\n            hasOptionInCommandResult parseResult.CommandResult optionName\n\n    /// Checks if an option was implicitly specified (i.e. the default value was used), or explicitly specified by the user.\n    let isOptionResultImplicit (parseResult: ParseResult) (optionName: string) =\n        if isOptionPresent parseResult optionName then\n            let result = parseResult.GetResult(optionName)\n\n            if isNull result then\n                true\n            else\n                let option = result :?> OptionResult\n                option.Implicit\n        else\n            false\n\n    let resolveCorrelationId (parseResult: ParseResult) : CorrelationId =\n        if isOptionPresent parseResult OptionName.CorrelationId\n           && not\n              <| isOptionResultImplicit parseResult OptionName.CorrelationId then\n            parseResult.GetValue(OptionName.CorrelationId)\n        else\n            match invocationCorrelationId with\n            | Some correlationId -> correlationId\n            | None ->\n                let correlationId = generateCorrelationId ()\n                invocationCorrelationId <- Some correlationId\n                correlationId\n\n    /// Adjusts command-line options to account for whether Id's or Name's were explicitly specified by the user,\n    ///    or should be taken from default values.\n    let getNormalizedIdsAndNames (parseResult: ParseResult) =\n\n        let isExplicitName (nameOption: string) =\n            isOptionPresent parseResult nameOption\n            && not\n               <| isOptionResultImplicit parseResult nameOption\n            && not\n               <| String.IsNullOrWhiteSpace(parseResult.GetValue<string>(nameOption))\n\n        let needsFallback (idOption: string) (nameOption: string) =\n            isOptionPresent parseResult idOption\n            && isOptionResultImplicit parseResult idOption\n            && parseResult.GetValue<Guid>(idOption) = Guid.Empty\n            && not <| isExplicitName nameOption\n\n        let needsConfigFallback =\n            needsFallback OptionName.OwnerId OptionName.OwnerName\n            || needsFallback OptionName.OrganizationId OptionName.OrganizationName\n            || needsFallback OptionName.RepositoryId OptionName.RepositoryName\n            || needsFallback OptionName.BranchId OptionName.BranchName\n\n        let config = if needsConfigFallback then Some(Current()) else None\n\n        let getNormalizedId (idOption: string) (nameOption: string) (configValue: Guid) =\n            let isImplicit = isOptionResultImplicit parseResult idOption\n            let explicitName = isExplicitName nameOption\n            let idValue = parseResult.GetValue<Guid>(idOption)\n\n            if isImplicit && explicitName then\n                Guid.Empty\n            elif isImplicit\n                 && idValue = Guid.Empty\n                 && not explicitName then\n                configValue\n            else\n                idValue\n\n        // If the name was specified on the command line, but the id wasn't (i.e. the default value was specified, and Implicit = true),\n        //   then we should only send the name, and we set the id to Guid.Empty.\n\n        let mutable graceIds = GraceIds.Default\n\n        if isOptionPresent parseResult OptionName.CorrelationId then\n            graceIds <- { graceIds with CorrelationId = resolveCorrelationId parseResult }\n\n        if isOptionPresent parseResult OptionName.OwnerId\n           || isOptionPresent parseResult OptionName.OwnerName then\n            let ownerId =\n                let configValue =\n                    config\n                    |> Option.map (fun current -> current.OwnerId)\n                    |> Option.defaultValue OwnerId.Empty\n\n                getNormalizedId OptionName.OwnerId OptionName.OwnerName configValue\n\n            graceIds <-\n                { graceIds with\n                    OwnerId = ownerId\n                    OwnerIdString = if ownerId = Guid.Empty then \"\" else $\"{ownerId}\"\n                    OwnerName = parseResult.GetValue<string>(OptionName.OwnerName)\n                    HasOwner = true\n                }\n\n        if isOptionPresent parseResult OptionName.OrganizationId\n           || isOptionPresent parseResult OptionName.OrganizationName then\n            let organizationId =\n                let configValue =\n                    config\n                    |> Option.map (fun current -> current.OrganizationId)\n                    |> Option.defaultValue OrganizationId.Empty\n\n                getNormalizedId OptionName.OrganizationId OptionName.OrganizationName configValue\n\n            graceIds <-\n                { graceIds with\n                    OrganizationId = organizationId\n                    OrganizationIdString = if organizationId = Guid.Empty then \"\" else $\"{organizationId}\"\n                    OrganizationName = parseResult.GetValue<string>(OptionName.OrganizationName)\n                    HasOrganization = true\n                }\n\n        if isOptionPresent parseResult OptionName.RepositoryId\n           || isOptionPresent parseResult OptionName.RepositoryName then\n            let repositoryId =\n                let configValue =\n                    config\n                    |> Option.map (fun current -> current.RepositoryId)\n                    |> Option.defaultValue RepositoryId.Empty\n\n                getNormalizedId OptionName.RepositoryId OptionName.RepositoryName configValue\n\n            graceIds <-\n                { graceIds with\n                    RepositoryId = repositoryId\n                    RepositoryIdString = if repositoryId = Guid.Empty then \"\" else $\"{repositoryId}\"\n                    RepositoryName = parseResult.GetValue<string>(OptionName.RepositoryName)\n                    HasRepository = true\n                }\n\n        if isOptionPresent parseResult OptionName.BranchId\n           || isOptionPresent parseResult OptionName.BranchName then\n            let branchId =\n                let configValue =\n                    config\n                    |> Option.map (fun current -> current.BranchId)\n                    |> Option.defaultValue BranchId.Empty\n\n                getNormalizedId OptionName.BranchId OptionName.BranchName configValue\n\n            graceIds <-\n                { graceIds with\n                    BranchId = branchId\n                    BranchIdString = if branchId = Guid.Empty then \"\" else $\"{branchId}\"\n                    BranchName = parseResult.GetValue<string>(OptionName.BranchName)\n                    HasBranch = true\n                }\n\n        graceIds\n"
  },
  {
    "path": "src/Grace.CLI/Command/Watch.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.SDK\nopen Grace.SDK.Common\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Parameters.Branch\nopen Grace.Shared.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http.Connections\nopen Microsoft.AspNetCore.SignalR.Client\nopen NodaTime\nopen Spectre.Console\nopen System\nopen System.Buffers\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.ComponentModel\nopen System.Diagnostics\nopen System.Globalization\nopen System.IO\nopen System.IO.Compression\nopen System.IO.Enumeration\nopen System.Linq\nopen System.Net\nopen System.Net.Http\nopen System.Reactive.Linq\nopen System.Security.Cryptography\nopen System.Text.Json\nopen System.Threading.Tasks\nopen System.Threading\nopen System.Collections.Concurrent\nopen Spectre.Console\nopen System.Text\nopen Grace.Shared.Parameters.Storage\nopen Grace.CLI.Text\nopen Grace.Types.Automation\n\nmodule Watch =\n\n    module private Options =\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<String>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The repository's organization ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<String>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The repository's organization name. [default: current organization]\",\n                Arity = ArgumentArity.ZeroOrOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                [| \"-r\" |],\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<String>(\n                OptionName.RepositoryName,\n                [| \"-n\" |],\n                Required = false,\n                Description = \"The name of the repository. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let branchId =\n            new Option<BranchId>(\n                OptionName.BranchId,\n                [| \"-i\" |],\n                Required = false,\n                Description = \"The branch's ID <Guid>.\",\n                Arity = ArgumentArity.ExactlyOne,\n                DefaultValueFactory = (fun _ -> BranchId.Empty)\n            )\n\n        let branchName =\n            new Option<String>(\n                OptionName.BranchName,\n                [| \"-b\" |],\n                Required = false,\n                Description = \"The name of the branch. [default: current branch]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    /// Holds a list of the created or changed files that we need to process, as determined by the FileSystemWatcher.\n    ///\n    /// Note: We're using ConcurrentDictionary because it's safe for multithreading, doesn't allow us to insert the same key twice, and for its algorithms. We're not using the values of the ConcurrentDictionary here, only the keys.\n    let private filesToProcess = ConcurrentDictionary<string, unit>()\n\n    /// Holds a list of the created or changed directories that we need to process, as determined by the FileSystemWatcher.\n    ///\n    /// Note: We're using ConcurrentDictionary because it's safe for multithreading, doesn't allow us to insert the same key twice, and for its algorithms. We're not using the values of the ConcurrentDictionary here, only the keys.\n    let private directoriesToProcess = ConcurrentDictionary<string, unit>()\n\n    type WatchParameters() =\n        inherit ParameterBase()\n        member val public RepositoryPath: string = String.Empty with get, set\n        member val public NamedSections: string [] = Array.empty with get, set\n\n    let mutable private graceStatus = GraceStatus.Default\n    let mutable private graceStatusDirectoryIds = HashSet<DirectoryVersionId>()\n    let mutable graceStatusMemoryStream: MemoryStream = null\n    let mutable graceStatusHasChanged = false\n\n    let fileDeleted filePath = logToConsole $\"In Delete: filePath: {filePath}\"   \n\n    let isNotDirectory path = not <| Directory.Exists(path)\n    let updateInProgress () = File.Exists(updateInProgressFileName ())\n    let updateNotInProgress () = not <| updateInProgress ()\n\n    let resolveSignalRAccessTokenResult (tokenResult: Result<string option, string>) =\n        match tokenResult with\n        | Ok(Some token) when not (String.IsNullOrWhiteSpace token) -> Ok token\n        | Ok _ ->\n            Error\n                $\"No access token is available. Run `grace auth login` or set {Constants.EnvironmentVariables.GraceToken} before starting watch.\"\n        | Error message -> Error $\"Unable to acquire an access token for SignalR notifications: {message}\"\n\n    let private getSignalRAccessToken () =\n        task {\n            let! tokenResult = Grace.CLI.Command.Auth.tryGetAccessToken ()\n            return resolveSignalRAccessTokenResult tokenResult\n        }\n\n    let private createSignalRConnection (signalRUrl: Uri) =\n        HubConnectionBuilder()\n            .WithAutomaticReconnect()\n            .WithUrl(\n                signalRUrl,\n                fun options ->\n                    options.Transports <- HttpTransportType.ServerSentEvents\n\n                    options.AccessTokenProvider <-\n                        Func<Task<string>>(fun () ->\n                            task {\n                                let! accessTokenResult = getSignalRAccessToken ()\n\n                                match accessTokenResult with\n                                | Ok accessToken -> return accessToken\n                                | Error message -> return raise (InvalidOperationException(message))\n                            })\n            )\n            .Build()\n\n    let private isGraceStatusArtifact (fullPath: string) =\n        let statusFile = Current().GraceStatusFile\n        fullPath.Equals(statusFile, StringComparison.InvariantCultureIgnoreCase)\n        || fullPath.Equals(statusFile + \"-wal\", StringComparison.InvariantCultureIgnoreCase)\n        || fullPath.Equals(statusFile + \"-shm\", StringComparison.InvariantCultureIgnoreCase)\n        || fullPath.Equals(statusFile + \"-journal\", StringComparison.InvariantCultureIgnoreCase)\n\n    let OnCreated (args: FileSystemEventArgs) =\n        // Ignore directory creation; need to think about this more... should we capture new empty directories?\n        if updateNotInProgress ()\n           && isNotDirectory args.FullPath then\n            let shouldIgnore = shouldIgnoreFile args.FullPath\n            //logToAnsiConsole Colors.Verbose $\"Should ignore {args.FullPath}: {shouldIgnore}.\"\n\n            if not <| shouldIgnore then\n                logToAnsiConsole Colors.Added $\"I saw that {args.FullPath} was created.\"\n                filesToProcess.TryAdd(args.FullPath, ()) |> ignore\n\n            if (isGraceStatusArtifact args.FullPath)\n               && (not <| graceStatusHasChanged) then\n                graceStatusHasChanged <- true\n\n    let OnChanged (args: FileSystemEventArgs) =\n        if updateNotInProgress ()\n           && isNotDirectory args.FullPath then\n            let shouldIgnore = shouldIgnoreFile args.FullPath\n            //logToAnsiConsole Colors.Verbose $\"Should ignore {args.FullPath}: {shouldIgnore}.\"\n\n            if not <| shouldIgnore then\n                logToAnsiConsole Colors.Changed $\"I saw that {args.FullPath} changed.\"\n                filesToProcess.TryAdd(args.FullPath, ()) |> ignore\n\n            // Special handling for the Grace status file; if that is the changed file, we'll set this flag so we reload it in OnWatch() in the main loop\n            if (isGraceStatusArtifact args.FullPath)\n               && (not <| graceStatusHasChanged) then\n                //logToAnsiConsole Colors.Important $\"Setting graceStatusHasChanged to true in OnChanged(). Current value: {graceStatusHasChanged}.\"\n                graceStatusHasChanged <- true\n                logToAnsiConsole Colors.Important $\"Grace Status file has been updated.\"\n\n    let OnDeleted (args: FileSystemEventArgs) =\n        if updateNotInProgress ()\n           && isNotDirectory args.FullPath then\n            let shouldIgnore = shouldIgnoreFile args.FullPath\n            //logToAnsiConsole Colors.Verbose $\"Should ignore {args.FullPath}: {shouldIgnore}.\"\n\n            if not <| shouldIgnore then\n                logToAnsiConsole Colors.Deleted $\"I saw that {args.FullPath} was deleted.\"\n                logToAnsiConsole Colors.Deleted $\"Delete processing is not yet implemented.\"\n\n            if (isGraceStatusArtifact args.FullPath)\n               && (not <| graceStatusHasChanged) then\n                graceStatusHasChanged <- true\n\n    let OnRenamed (args: RenamedEventArgs) =\n        if updateNotInProgress () then\n            let shouldIgnoreOldFile = shouldIgnoreFile args.OldFullPath\n            let shouldIgnoreNewFile = shouldIgnoreFile args.FullPath\n\n            if not <| shouldIgnoreOldFile then\n                logToAnsiConsole Colors.Changed $\"I saw that {args.OldFullPath} was renamed to {args.FullPath}.\"\n                //logToAnsiConsole Colors.Verbose $\"Should ignore {args.OldFullPath}: {shouldIgnoreOldFile}. Should ignore {args.FullPath}: {shouldIgnoreNewFile}.\"\n                logToAnsiConsole Colors.Changed $\"Delete processing is not yet implemented.\"\n\n            if not <| shouldIgnoreNewFile then\n                logToAnsiConsole Colors.Changed $\"I saw that {args.OldFullPath} was renamed to {args.FullPath}.\"\n                //logToAnsiConsole Colors.Verbose $\"Should ignore {args.OldFullPath}: {shouldIgnoreOldFile}. Should ignore {args.FullPath}: {shouldIgnoreNewFile}.\"\n                filesToProcess.TryAdd(args.FullPath, ()) |> ignore\n\n    let OnError (args: ErrorEventArgs) =\n        let correlationId = generateCorrelationId ()\n\n        logToAnsiConsole Colors.Error $\"I saw that the FileSystemWatcher threw an exception: {args.GetException().Message}. grace watch should be restarted.\"\n\n    let OnGraceUpdateInProgressCreated (args: FileSystemEventArgs) =\n        if args.FullPath = updateInProgressFileName () then\n            if updateInProgress () then\n                logToAnsiConsole Colors.Important $\"Update is in progress from another Grace instance.\"\n            else\n                logToAnsiConsole Colors.Important $\"{updateInProgressFileName ()} should already exist, but it doesn't.\"\n\n    let OnGraceUpdateInProgressDeleted (args: FileSystemEventArgs) =\n        if args.FullPath = updateInProgressFileName () then\n            if updateNotInProgress () then\n                logToAnsiConsole Colors.Important $\"Update has finished in another Grace instance.\"\n            else\n                logToAnsiConsole Colors.Important $\"{updateInProgressFileName ()} should have been deleted, but it hasn't yet.\"\n\n    /// Creates a FileSystemWatcher for the given path.\n    let createFileSystemWatcher path =\n        let fileSystemWatcher = new FileSystemWatcher(path)\n        fileSystemWatcher.InternalBufferSize <- (64 * 1024) // Default is 4K, choosing maximum of 64K for safety.\n        fileSystemWatcher.IncludeSubdirectories <- true\n\n        fileSystemWatcher.NotifyFilter <-\n            NotifyFilters.DirectoryName\n            ||| NotifyFilters.FileName\n            ||| NotifyFilters.LastWrite\n            ||| NotifyFilters.Security\n\n        fileSystemWatcher\n\n    let printDifferences (differences: List<FileSystemDifference>) =\n        if differences.Count > 0 then\n            logToAnsiConsole Colors.Verbose $\"Differences detected since last save/checkpoint/commit:\"\n\n        for difference in differences.OrderBy(fun diff -> diff.RelativePath) do\n            logToAnsiConsole\n                Colors.Verbose\n                $\"{getDiscriminatedUnionCaseName difference.DifferenceType} for {getDiscriminatedUnionCaseName difference.FileSystemEntryType} {difference.RelativePath}\"\n\n    /// Update the Grace Object Cache file with the new DirectoryVersions.\n    let updateObjectCacheFile (newDirectoryVersions: List<LocalDirectoryVersion>) =\n        task { do! upsertObjectCache newDirectoryVersions }\n\n    /// Updates the Grace Status file's Index with updates detected from the file system.\n    let updateGraceStatus graceStatus correlationId =\n        task {\n            // Get the list of differences between what's in the working directory, and what Grace Index knows about.\n            let! differences = scanForDifferences graceStatus\n            printDifferences differences\n\n            // Get an updated Grace Index, and any new DirectoryVersions that were needed to build it.\n            let! (newGraceStatus, newDirectoryVersions) = getNewGraceStatusAndDirectoryVersions graceStatus differences\n\n            // Log the changes.\n            for dv in newDirectoryVersions do\n                logToAnsiConsole\n                    Colors.Verbose\n                    $\"new Sha256Hash: {dv.Sha256Hash.Substring(0, 8)}; DirectoryId: {dv.DirectoryVersionId.ToString().Substring(0, 9)}...; RelativePath: {dv.RelativePath}\"\n\n            // Upload the new directory versions.\n            let! result = uploadDirectoryVersions newDirectoryVersions correlationId\n\n            match result with\n            | Ok returnValue ->\n                do! updateObjectCacheFile newDirectoryVersions\n\n                let fileDifferences =\n                    differences\n                        .Where(fun diff -> diff.FileSystemEntryType = FileSystemEntryType.File)\n                        .ToList()\n\n                let message =\n                    if fileDifferences |> Seq.isEmpty then\n                        String.Empty\n                    else\n                        let sb = stringBuilderPool.Get()\n\n                        try\n                            for fileDifference in fileDifferences do\n                                //sb.AppendLine($\"{(getDiscriminatedUnionCaseNameToString fileDifference.DifferenceType)}: {fileDifference.RelativePath}\") |> ignore\n                                match fileDifference.DifferenceType with\n                                | Change ->\n                                    sb.AppendLine($\"{fileDifference.RelativePath}\")\n                                    |> ignore\n                                | Add ->\n                                    sb.AppendLine($\"Add {fileDifference.RelativePath}\")\n                                    |> ignore\n                                | Delete ->\n                                    sb.AppendLine($\"Delete {fileDifference.RelativePath}\")\n                                    |> ignore\n\n                            let saveMessage = sb.ToString()\n                            saveMessage.Remove(saveMessage.LastIndexOf(Environment.NewLine), Environment.NewLine.Length)\n                        finally\n                            stringBuilderPool.Return(sb)\n\n                // If there are changes either to files or just to directories, create a save reference.\n                if (differences.Count > 0) then\n                    match! createSaveReference (getRootDirectoryVersion newGraceStatus) message correlationId with\n                    | Ok returnValue ->\n                        let newGraceStatusWithUpdatedTime =\n                            { newGraceStatus with LastSuccessfulDirectoryVersionUpload = getCurrentInstant () }\n                        // Apply incremental changes to the Grace Status DB.\n                        do! applyGraceStatusIncremental newGraceStatusWithUpdatedTime newDirectoryVersions differences\n                        //logToAnsiConsole Colors.Important $\"Setting graceStatusHasChanged to false in updateGraceStatus(). Current value: {graceStatusHasChanged}.\"\n                        graceStatusHasChanged <- false // We *just* changed it ourselves, so we don't have to re-process it in the timer loop.\n                        return Some newGraceStatusWithUpdatedTime\n                    | Error error ->\n                        logToAnsiConsole Colors.Error $\"{Markup.Escape(error.Error)}\"\n                        return None\n                else\n                    // There were no changes to process, so just return the existing GraceStatus.\n                    //logToAnsiConsole Colors.Verbose \"No fileDifferences or newDirectoryVersions to process; not updating GraceStatus.\"\n                    return Some graceStatus\n            | Error error ->\n                logToAnsiConsole Colors.Error $\"{Markup.Escape(error.Error)}\"\n                return None\n        }\n\n    /// Copies a file from the working directory to the object directory, with its SHA-256 hash, and then uploads it to storage.\n    let copyFileToObjectDirectoryAndUploadToStorage (getUploadMetadataForFilesParameters: GetUploadMetadataForFilesParameters) fullPath =\n        task {\n            //logToConsole $\"*In fileChanged for {fullPath}.\"\n            match! copyToObjectDirectory fullPath with\n            | Some fileVersion ->\n                getUploadMetadataForFilesParameters.FileVersions <- [| fileVersion |]\n\n                match! uploadFilesToObjectStorage getUploadMetadataForFilesParameters with\n                | Ok returnValue -> logToAnsiConsole Colors.Verbose $\"File {fileVersion.GetObjectFileName} has been uploaded to storage.\"\n                | Error error -> logToAnsiConsole Colors.Error $\"**Failed to upload {fileVersion.GetObjectFileName} to storage.\"\n            | None -> ()\n        }\n\n    /// Decompresses the GraceStatus information from the memory stream.\n    let retrieveGraceStatusFromMemoryStream () =\n        task {\n            logToAnsiConsole Colors.Verbose $\"Retrieving Grace Status from compressed memory stream.\"\n            graceStatusMemoryStream.Position <- 0\n            use gzStream = new GZipStream(graceStatusMemoryStream, CompressionMode.Decompress)\n\n            let! retrievedGraceStatus = JsonSerializer.DeserializeAsync<GraceStatus>(gzStream, Constants.JsonSerializerOptions)\n            graceStatus <- retrievedGraceStatus\n            logToAnsiConsole Colors.Verbose $\"Retrieved Grace Status from compressed memory stream.\"\n\n            do! gzStream.DisposeAsync() // Dispose the GZipStream first, before disposing the MemoryStream.\n            do! graceStatusMemoryStream.DisposeAsync()\n            graceStatusMemoryStream <- null\n        }\n\n    /// Compresses the GraceStatus information into a gzipped memory stream.\n    let storeGraceStatusInMemoryStream () =\n        task {\n            logToAnsiConsole Colors.Verbose $\"Storing Grace Status in compressed memory stream.\"\n            graceStatusMemoryStream <- new MemoryStream()\n\n            use gzStream = new GZipStream(graceStatusMemoryStream, CompressionLevel.SmallestSize, leaveOpen = true)\n\n            do! serializeAsync gzStream graceStatus\n            do! gzStream.FlushAsync()\n            do! graceStatusMemoryStream.FlushAsync()\n            logToAnsiConsole Colors.Verbose $\"Stored Grace Status in compressed memory stream.\"\n            do! gzStream.DisposeAsync()\n            graceStatus <- GraceStatus.Default\n        }\n\n    let updateGraceStatusDirectoryIds (status: GraceStatus) =\n        graceStatusDirectoryIds <- status.Index.Keys.ToHashSet()\n\n    /// Processes any changed files since the last timer tick.\n    let processChangedFiles () =\n        task {\n            // First, check if there's anything to process.\n            if\n                not\n                    (\n                        filesToProcess.IsEmpty\n                        && directoriesToProcess.IsEmpty\n                    )\n            then\n                try\n                    let correlationId = generateCorrelationId ()\n                    let! graceStatusFromDisk = readGraceStatusMeta ()\n                    graceStatus <- graceStatusFromDisk\n\n                    let mutable lastFileUploadInstant = graceStatus.LastSuccessfulFileUpload\n                    let mutable processedAnyFile = false\n\n                    /// This is just a way to throw away the unit value from the ConcurrentDictionary.\n                    let mutable unitValue = ()\n\n                    // Loop through no more than 50 files. Copy them to the objects directory, and upload them to storage.\n                    //   In the incredibly rare event that more than 50 files have changed, we'll get 50-per-timer-tick,\n                    //   and clear the queue quickly without overwhelming the system.\n                    let getUploadMetadataForFilesParameters =\n                        GetUploadMetadataForFilesParameters(\n                            OwnerId = $\"{Current().OwnerId}\",\n                            OrganizationId = $\"{Current().OrganizationId}\",\n                            RepositoryId = $\"{Current().RepositoryId}\",\n                            CorrelationId = correlationId\n                        )\n\n                    for fileName in filesToProcess.Keys.Take(50) do\n                        if filesToProcess.TryRemove(fileName, &unitValue) then\n                            logToAnsiConsole Colors.Verbose $\"Processing {fileName}. filesToProcess.Count: {filesToProcess.Count}.\"\n                            do! copyFileToObjectDirectoryAndUploadToStorage getUploadMetadataForFilesParameters (FilePath fileName)\n                            processedAnyFile <- true\n                            lastFileUploadInstant <- getCurrentInstant ()\n\n                    if processedAnyFile then\n                        graceStatus <- { graceStatus with LastSuccessfulFileUpload = lastFileUploadInstant }\n                        do! applyGraceStatusIncremental graceStatus Seq.empty Seq.empty\n\n                    // If we've drained all of the files that changed (and we'll almost always have done so), update all the things:\n                    //   GraceStatus, directory versions, etc.\n                    if filesToProcess.IsEmpty then\n                        let! graceStatusSnapshot = readGraceStatusFile ()\n                        graceStatus <- graceStatusSnapshot\n                        match! (updateGraceStatus graceStatus correlationId) with\n                        | Some newGraceStatus -> graceStatus <- newGraceStatus\n                        | None ->\n                            logToAnsiConsole Colors.Important $\"Grace Status file was not updated.\"\n                            () // Something went wrong, don't update the in-memory Grace Status.\n\n                    updateGraceStatusDirectoryIds graceStatus\n                    do! updateGraceWatchInterprocessFile graceStatus (Some graceStatusDirectoryIds)\n\n                    // Reset the in-memory Grace Status to empty to minimize memory usage.\n                    graceStatus <- GraceStatus.Default\n                    GC.Collect(2, GCCollectionMode.Forced, blocking = true, compacting = true)\n                with\n                | ex ->\n                    logToAnsiConsole\n                        Colors.Error\n                        $\"Error in processChangedFiles: Message: {ex.Message}{Environment.NewLine}{Environment.NewLine}{ex.StackTrace}\"\n            // Refresh the file every (just under) 5 minutes to indicate that `grace watch` is still alive.\n            elif\n                graceWatchStatusUpdateTime\n                < getCurrentInstant().Minus(Duration.FromMinutes(4.8))\n            then\n                let! graceStatusFromDisk = readGraceStatusMeta ()\n                do! updateGraceWatchInterprocessFile graceStatusFromDisk (Some graceStatusDirectoryIds)\n                GC.Collect(2, GCCollectionMode.Forced, blocking = true, compacting = true)\n        }\n\n    type Watch() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) =\n            task {\n                try\n                    // Create the FileSystemWatcher, but don't enable it yet.\n                    use rootDirectoryFileSystemWatcher = createFileSystemWatcher (Current().RootDirectory)\n\n                    use created =\n                        Observable\n                            .FromEventPattern<FileSystemEventArgs>(rootDirectoryFileSystemWatcher, \"Created\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnCreated)\n\n                    use changed =\n                        Observable\n                            .FromEventPattern<FileSystemEventArgs>(rootDirectoryFileSystemWatcher, \"Changed\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnChanged)\n\n                    use deleted =\n                        Observable\n                            .FromEventPattern<FileSystemEventArgs>(rootDirectoryFileSystemWatcher, \"Deleted\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnDeleted)\n\n                    use renamed =\n                        Observable\n                            .FromEventPattern<RenamedEventArgs>(rootDirectoryFileSystemWatcher, \"Renamed\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnRenamed)\n\n                    use errored =\n                        Observable\n                            .FromEventPattern<ErrorEventArgs>(rootDirectoryFileSystemWatcher, \"Error\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnError) // I want all of the errors.\n\n                    Directory.CreateDirectory(Path.GetDirectoryName(updateInProgressFileName ()))\n                    |> ignore\n\n                    use updateInProgressFileSystemWatcher = createFileSystemWatcher (Path.GetDirectoryName(updateInProgressFileName ()))\n\n                    use updateInProgressChanged =\n                        Observable\n                            .FromEventPattern<FileSystemEventArgs>(updateInProgressFileSystemWatcher, \"Created\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnGraceUpdateInProgressCreated)\n\n                    use updateInProgressDeleted =\n                        Observable\n                            .FromEventPattern<FileSystemEventArgs>(updateInProgressFileSystemWatcher, \"Deleted\")\n                            .Select(fun e -> e.EventArgs)\n                            .Subscribe(OnGraceUpdateInProgressDeleted)\n\n                    // Load the Grace Index file.\n                    let! status = readGraceStatusFile ()\n                    graceStatus <- status\n                    updateGraceStatusDirectoryIds graceStatus\n\n                    // Create the inter-process communication file.\n                    do! updateGraceWatchInterprocessFile graceStatus (Some graceStatusDirectoryIds)\n\n                    // Enable the FileSystemWatcher.\n                    rootDirectoryFileSystemWatcher.EnableRaisingEvents <- true\n                    updateInProgressFileSystemWatcher.EnableRaisingEvents <- true\n\n                    let timerTimeSpan = TimeSpan.FromSeconds(1.0)\n\n                    logToAnsiConsole Colors.Verbose $\"The change processor timer will tick every {timerTimeSpan.TotalSeconds:F1} seconds.\"\n\n                    // Open a SignalR connection to the server.\n                    let signalRUrl = Uri($\"{Current().ServerUri}/notifications\")\n                    logToConsole $\"signalRUrl: {signalRUrl}.\"\n\n                    match! getSignalRAccessToken () with\n                    | Error message -> raise (InvalidOperationException(message))\n                    | Ok _ -> ()\n\n                    use signalRConnection = createSignalRConnection signalRUrl\n\n                    let mutable watchedParentBranchId = BranchId.Empty\n\n                    use notifyRepository =\n                        signalRConnection.On<RepositoryId, ReferenceId>(\n                            \"NotifyRepository\",\n                            fun repositoryId referenceId ->\n                                (task { logToAnsiConsole Colors.Highlighted $\"ReferenceId {referenceId} was created in repository {repositoryId}.\" }) :> Task\n                        )\n\n                    use serverToClient =\n                        signalRConnection.On<string>(\n                            \"ServerToClientMessage\",\n                            (fun message -> logToAnsiConsole Colors.Important $\"From Grace Server: {message}\")\n                        )\n\n                    use notifyAutomationEvent =\n                        signalRConnection.On<AutomationEventEnvelope>(\n                            \"NotifyAutomationEvent\",\n                            fun envelope ->\n                                (task {\n                                    if envelope.EventType = AutomationEventType.PromotionSetApplied then\n                                        try\n                                            use document = JsonDocument.Parse(envelope.DataJson)\n                                            let root = document.RootElement\n\n                                            let tryParseGuidProperty (propertyName: string) : Guid option =\n                                                let mutable propertyValue = Unchecked.defaultof<JsonElement>\n\n                                                if root.TryGetProperty(propertyName, &propertyValue) then\n                                                    let propertyText = propertyValue.GetString()\n                                                    let mutable parsedGuid = Guid.Empty\n\n                                                    if\n                                                        String.IsNullOrWhiteSpace propertyText |> not\n                                                        && Guid.TryParse(propertyText, &parsedGuid)\n                                                    then\n                                                        Some parsedGuid\n                                                    else\n                                                        Option.None\n                                                else\n                                                    Option.None\n\n                                            let targetBranchId =\n                                                tryParseGuidProperty \"targetBranchId\"\n                                                |> Option.defaultValue BranchId.Empty\n\n                                            let terminalReferenceId =\n                                                tryParseGuidProperty \"terminalPromotionReferenceId\"\n                                                |> Option.defaultValue ReferenceId.Empty\n\n                                            if watchedParentBranchId = targetBranchId\n                                               && watchedParentBranchId <> BranchId.Empty then\n                                                logToAnsiConsole\n                                                    Colors.Highlighted\n                                                    $\"Parent branch {watchedParentBranchId} received terminal promotion {terminalReferenceId}; starting auto-rebase.\"\n\n                                                let! currentStatus = readGraceStatusFile ()\n                                                let! _ = Branch.rebaseHandler (parseResult |> getNormalizedIdsAndNames) currentStatus\n                                                ()\n                                        with\n                                        | ex ->\n                                            logToAnsiConsole\n                                                Colors.Error\n                                                $\"Failed to process automation event payload for {envelope.EventType}: {Markup.Escape(ex.Message)}.\"\n                                })\n                                :> Task\n                        )\n\n                    use notifyOnSave =\n                        signalRConnection.On<BranchName, BranchName, BranchId, ReferenceId>(\n                            \"NotifyOnSave\",\n                            fun branchName parentBranchName parentBranchId referenceId ->\n                                (task {\n                                    logToAnsiConsole\n                                        Colors.Highlighted\n                                        $\"Branch {branchName} with parent branch {parentBranchName} has a new save; referenceId: {referenceId}.\"\n                                })\n                                :> Task\n                        )\n\n                    use notifyOnCheckpoint =\n                        signalRConnection.On<BranchName, BranchName, BranchId, ReferenceId>(\n                            \"NotifyOnCheckpoint\",\n                            fun branchName parentBranchName parentBranchId referenceId ->\n                                (task {\n                                    logToAnsiConsole\n                                        Colors.Highlighted\n                                        $\"Branch {branchName} with parent branch {parentBranchName} has a new checkpoint; referenceId: {referenceId}.\"\n                                })\n                                :> Task\n                        )\n\n                    use notifyOnCommit =\n                        signalRConnection.On<BranchName, BranchName, BranchId, ReferenceId>(\n                            \"NotifyOnCommit\",\n                            fun branchName parentBranchName parentBranchId referenceId ->\n                                (task {\n                                    logToAnsiConsole\n                                        Colors.Highlighted\n                                        $\"Branch {branchName} with parent branch {parentBranchName} has a new commit; referenceId: {referenceId}.\"\n                                })\n                                :> Task\n                        )\n\n                    signalRConnection.add_Closed (fun ex -> task { logToAnsiConsole Colors.Error $\"SignalR connection closed: {ex.Message}.\" })\n\n                    signalRConnection.add_Reconnecting (fun ex -> task { logToAnsiConsole Colors.Important $\"SignalR connection reconnecting: {ex.Message}.\" })\n\n                    signalRConnection.add_Reconnected (fun connectionId ->\n                        task { logToAnsiConsole Colors.Important $\"SignalR connection reconnected: {connectionId}.\" })\n\n                    do! signalRConnection.StartAsync(cancellationToken)\n                    do! signalRConnection.InvokeAsync(\"RegisterRepository\", Current().RepositoryId, cancellationToken)\n\n                    logToAnsiConsole\n                        Colors.Highlighted\n                        $\"SignalR Hub connection state: {signalRConnection.State}. Listening for changes in repository {Current().RepositoryName} ({Current().RepositoryId}); connectionId: {signalRConnection.ConnectionId}.\"\n\n                    // Get the parent BranchId so we can tell SignalR what to notify us about.\n                    let branchGetParameters =\n                        GetBranchParameters(\n                            OwnerId = $\"{Current().OwnerId}\",\n                            OrganizationId = $\"{Current().OrganizationId}\",\n                            RepositoryId = $\"{Current().RepositoryId}\",\n                            BranchId = $\"{Current().BranchId}\"\n                        )\n\n                    match! Branch.GetParentBranch branchGetParameters with\n                    | Ok returnValue ->\n                        let parentBranchDto = returnValue.ReturnValue\n                        watchedParentBranchId <- parentBranchDto.BranchId\n\n                        do! signalRConnection.InvokeAsync(\"RegisterParentBranch\", Current().BranchId, parentBranchDto.BranchId, cancellationToken)\n\n                        logToAnsiConsole\n                            Colors.Highlighted\n                            $\"SignalR Hub connection state: {signalRConnection.State}. Listening for changes in parent branch {parentBranchDto.BranchName} ({parentBranchDto.BranchId}); connectionId: {signalRConnection.ConnectionId}.\"\n                    | Error error ->\n                        logToAnsiConsole Colors.Error $\"Failed to retrieve branch metadata. Cannot connect to SignalR Hub.\"\n\n                        logToAnsiConsole Colors.Error $\"{Markup.Escape(error.ToString())}\"\n\n                    // Check for changes that occurred while not running.\n                    logToAnsiConsole Colors.Verbose $\"Scanning for differences.\"\n                    let! differences = scanForDifferences graceStatus // <--- This always finds the directories with updated write times, but we never update GraceStatus below..\n\n                    if differences |> Seq.isEmpty then\n                        logToAnsiConsole Colors.Verbose $\"Already up-to-date.\"\n                    else\n                        logToAnsiConsole Colors.Verbose $\"Found {differences.Count} differences.\"\n\n                    for difference in differences do\n                        match difference.FileSystemEntryType with\n                        | Directory ->\n                            directoriesToProcess.TryAdd(difference.RelativePath, ())\n                            |> ignore\n                        | File ->\n                            filesToProcess.TryAdd(difference.RelativePath, ())\n                            |> ignore\n\n                    // Process any changes that occurred while not running.\n                    graceStatus <- GraceStatus.Default\n                    do! processChangedFiles ()\n\n                    // Create a timer to process the file changes detected by the FileSystemWatcher.\n                    // This timer is the reason that there's a delay in stopping `grace watch`.\n                    logToAnsiConsole Colors.Verbose $\"Starting timer.\"\n                    use periodicTimer = new PeriodicTimer(timerTimeSpan)\n                    let! tick = periodicTimer.WaitForNextTickAsync()\n                    let mutable previousGC = getCurrentInstant ()\n                    let mutable ticked = true\n\n                    while ticked\n                          && not (cancellationToken.IsCancellationRequested) do\n                        // Grace Status may have changed from branch switch, or other commands.\n                        if graceStatusHasChanged then\n                            let! updatedGraceStatus = readGraceStatusFile ()    \n                            graceStatus <- updatedGraceStatus\n                            updateGraceStatusDirectoryIds graceStatus\n                            do! updateGraceWatchInterprocessFile graceStatus (Some graceStatusDirectoryIds)\n                            //logToAnsiConsole Colors.Important $\"Setting graceStatusHasChanged to false in OnWatch(). Current value: {graceStatusHasChanged}.\"\n                            graceStatusHasChanged <- false\n\n                        do! processChangedFiles ()\n                        let! tick = periodicTimer.WaitForNextTickAsync()\n                        ticked <- tick\n\n                        // About once a minute, do a full GC to be kind with our memory usage. This is for looks, not for function.\n                        //\n                        // In .NET, when a computer has lots of available memory, and there's no memory pressure signal from the OS, GC doesn't happen much, if at all.\n                        //   With no memory pressure, `grace watch` wouldn't bother releasing its unused heap after handling events like saves and auto-rebases.\n                        //   Seeing that kind of memory usage could lead to uninformed people saying things like, \"OMG, `grace watch` takes up so much memory!\"\n                        //   Actually, `grace watch` only grabs a lot of memory at the moment of processing events. As soon as we're done, we want to release that\n                        //   memory back to the OS, that means forcing a full GC.\n                        //\n                        // Because of DATAS (see https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/datas), it may take more than one GC.Collect()\n                        //   call to fully compact the heap (and that's OK). If we weren't being so aggressive about memory usage, we would just let DATAS compute\n                        //   a close-to-optimal heap size on its own over time.\n                        if\n                            previousGC\n                            < getCurrentInstant().Minus(Duration.FromMinutes(1.0))\n                        then\n                            //let memoryBeforeGC = Process.GetCurrentProcess().WorkingSet64\n                            GC.Collect(2, GCCollectionMode.Forced, blocking = true, compacting = true)\n                            //logToAnsiConsole Colors.Verbose $\"Memory before GC: {memoryBeforeGC:N0}; after: {Process.GetCurrentProcess().WorkingSet64:N0}.\"\n                            previousGC <- getCurrentInstant ()\n\n                    return 0\n                with\n                | :? HttpRequestException as httpEx when\n                    httpEx.StatusCode.HasValue\n                    && httpEx.StatusCode.Value = HttpStatusCode.Unauthorized ->\n                    logToAnsiConsole\n                        Colors.Error\n                        $\"SignalR negotiation failed with 401 Unauthorized. Run `grace auth login` or set {Constants.EnvironmentVariables.GraceToken}, then retry `grace watch`.\"\n                    return -1\n                | :? InvalidOperationException as invalidOperationException\n                    when invalidOperationException.Message.Contains(\"access token\", StringComparison.OrdinalIgnoreCase) ->\n                    logToAnsiConsole Colors.Error $\"{Markup.Escape(invalidOperationException.Message)}\"\n                    return -1\n                | ex ->\n                    //let exceptionMarkup = Markup.Escape($\"{ExceptionResponse.Create ex}\").Replace(\"\\\\\\\\\", @\"\\\").Replace(\"\\r\\n\", Environment.NewLine)\n                    //logToAnsiConsole Colors.Error $\"{exceptionMarkup}\"\n                    let exceptionSettings = ExceptionSettings()\n                    // Need to fill in some exception styles here.\n                    exceptionSettings.Format <- ExceptionFormats.Default\n                    AnsiConsole.WriteException(ex, exceptionSettings)\n                    return -1\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n            |> addOption Options.branchName\n            |> addOption Options.branchId\n\n        // Create main command and aliases, if any.\n        let watchCommand =\n            new Command(\"watch\", Description = \"Watches your repo for changes, and uploads new versions of your files.\")\n            |> addCommonOptions\n\n        watchCommand.Aliases.Add(\"w\")\n        watchCommand.Action <- Watch()\n        watchCommand\n"
  },
  {
    "path": "src/Grace.CLI/Command/WorkItem.CLI.fs",
    "content": "namespace Grace.CLI.Command\n\nopen Azure.Storage.Blobs\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Artifact\nopen Grace.Types.WorkItem\nopen Grace.Types.Types\nopen Spectre.Console\nopen Spectre.Console.Json\nopen System\nopen System.CommandLine\nopen System.CommandLine.Invocation\nopen System.CommandLine.Parsing\nopen System.IO\nopen System.Security.Cryptography\nopen System.Text\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule WorkItemCommand =\n\n    module private Options =\n        let workItemId =\n            new Option<string>(\n                \"--work-item-id\",\n                [| \"--work-item\"; \"-w\" |],\n                Required = false,\n                Description = \"The work item ID <Guid>. Used only on create to override the generated ID.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let title = new Option<string>(\"--title\", Required = true, Description = \"Title for the work item.\", Arity = ArgumentArity.ExactlyOne)\n\n        let description =\n            new Option<string>(\n                OptionName.Description,\n                [| \"-d\" |],\n                Required = false,\n                Description = \"Description for the work item.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let statusSet =\n            (new Option<string>(\"--set\", Required = true, Description = \"Set the work item status.\", Arity = ArgumentArity.ExactlyOne))\n                .AcceptOnlyFromAmong(listCases<WorkItemStatus> ())\n\n        let file =\n            new Option<string>(\n                \"--file\",\n                [| \"-f\" |],\n                Required = false,\n                Description = \"Read attachment content from this file path.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let text =\n            new Option<string>(\"--text\", [| \"-t\" |], Required = false, Description = \"Attach inline text content directly.\", Arity = ArgumentArity.ExactlyOne)\n\n        let stdin = new Option<bool>(\"--stdin\", Required = false, Description = \"Read attachment content from standard input.\", Arity = ArgumentArity.ZeroOrOne)\n\n        let attachmentType =\n            (new Option<string>(\n                \"--type\",\n                Required = true,\n                Description = \"Attachment type to target: summary, prompt, or notes.\",\n                Arity = ArgumentArity.ExactlyOne\n            ))\n                .AcceptOnlyFromAmong([| \"summary\"; \"prompt\"; \"notes\" |])\n\n        let latest =\n            new Option<bool>(\n                \"--latest\",\n                Required = false,\n                Description = \"Select the most recently created attachment for the requested type.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> false)\n            )\n\n        let artifactId = new Option<string>(\"--artifact-id\", Required = true, Description = \"Attachment artifact ID <Guid>.\", Arity = ArgumentArity.ExactlyOne)\n\n        let outputFile =\n            new Option<string>(\n                \"--output-file\",\n                [| \"-f\" |],\n                Required = true,\n                Description = \"Write downloaded attachment bytes to this file path.\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let ownerId =\n            new Option<OwnerId>(\n                OptionName.OwnerId,\n                Required = false,\n                Description = \"The repository's owner ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OwnerId.Empty)\n            )\n\n        let ownerName =\n            new Option<string>(\n                OptionName.OwnerName,\n                Required = false,\n                Description = \"The repository's owner name. [default: current owner]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let organizationId =\n            new Option<OrganizationId>(\n                OptionName.OrganizationId,\n                Required = false,\n                Description = \"The organization's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> OrganizationId.Empty)\n            )\n\n        let organizationName =\n            new Option<string>(\n                OptionName.OrganizationName,\n                Required = false,\n                Description = \"The organization's name. [default: current organization]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n        let repositoryId =\n            new Option<RepositoryId>(\n                OptionName.RepositoryId,\n                Required = false,\n                Description = \"The repository's ID <Guid>.\",\n                Arity = ArgumentArity.ZeroOrOne,\n                DefaultValueFactory = (fun _ -> RepositoryId.Empty)\n            )\n\n        let repositoryName =\n            new Option<string>(\n                OptionName.RepositoryName,\n                Required = false,\n                Description = \"The repository's name. [default: current repository]\",\n                Arity = ArgumentArity.ExactlyOne\n            )\n\n    module private Arguments =\n        let workItemIdentifier = new Argument<string>(\"work-item\", Description = \"Work item ID <Guid> or work item number <positive integer>.\")\n\n        let referenceId = new Argument<string>(\"reference-id\", Description = \"Reference ID <Guid>.\")\n\n        let promotionSetId = new Argument<string>(\"promotion-set-id\", Description = \"Promotion set ID <Guid>.\")\n\n    type private AttachmentInput = { Bytes: byte array; MimeType: string }\n\n    type private AttachmentResult = { WorkItem: string; ArtifactId: ArtifactId; ArtifactType: string }\n\n    type private AttachmentDownloadResult = { WorkItem: string; ArtifactId: ArtifactId; AttachmentType: string; OutputFile: string; Size: int64 }\n\n    let private tryParseGuid (value: string) (error: WorkItemError) (parseResult: ParseResult) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value)\n           || Guid.TryParse(value, &parsed) = false\n           || parsed = Guid.Empty then\n            Error(GraceError.Create (WorkItemError.getErrorMessage error) (getCorrelationId parseResult))\n        else\n            Ok parsed\n\n    let private tryNormalizeWorkItemIdentifier (value: string) (parseResult: ParseResult) =\n        let mutable parsedGuid = Guid.Empty\n\n        if String.IsNullOrWhiteSpace(value) then\n            Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemId) (getCorrelationId parseResult))\n        elif\n            Guid.TryParse(value, &parsedGuid)\n            && parsedGuid <> Guid.Empty\n        then\n            Ok(parsedGuid.ToString())\n        else\n            let mutable parsedNumber = 0L\n\n            if Int64.TryParse(value, &parsedNumber) then\n                if parsedNumber > 0L then\n                    Ok(parsedNumber.ToString())\n                else\n                    Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemNumber) (getCorrelationId parseResult))\n            else\n                Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemId) (getCorrelationId parseResult))\n\n    let private createWorkItemWithProgress (parameters: Parameters.WorkItem.CreateWorkItemParameters) =\n        progress\n            .Columns(progressColumns)\n            .StartAsync(fun progressContext ->\n                task {\n                    let t0 = progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                    let! result = WorkItem.Create(parameters)\n                    t0.Increment(100.0)\n                    return result\n                })\n\n    let private inferMimeTypeFromFilePath (filePath: string) =\n        match Path.GetExtension(filePath).ToLowerInvariant() with\n        | \".md\" -> \"text/markdown\"\n        | \".txt\" -> \"text/plain\"\n        | \".json\" -> \"application/json\"\n        | _ -> \"application/octet-stream\"\n\n    let private computeSha256 (contentBytes: byte array) =\n        use hasher = SHA256.Create()\n        let hash = hasher.ComputeHash(contentBytes)\n        Convert.ToHexString(hash).ToLowerInvariant()\n\n    let private uploadArtifactContent (uploadUri: UriWithSharedAccessSignature) (contentBytes: byte array) =\n        task {\n            use stream = new MemoryStream(contentBytes)\n            let blobClient = BlobClient(uploadUri)\n            let! _ = blobClient.UploadAsync(stream, overwrite = true)\n            return ()\n        }\n\n    let private tryGetAttachmentInput (parseResult: ParseResult) =\n        task {\n            let filePath =\n                parseResult.GetValue(Options.file)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            let textInput =\n                parseResult.GetValue(Options.text)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            let readFromStdin = parseResult.GetValue(Options.stdin)\n\n            let selectedCount =\n                (if String.IsNullOrWhiteSpace(filePath) then 0 else 1)\n                + (if String.IsNullOrWhiteSpace(textInput) then 0 else 1)\n                + (if readFromStdin then 1 else 0)\n\n            if selectedCount <> 1 then\n                return Error(GraceError.Create \"Specify exactly one of --file, --text, or --stdin.\" (getCorrelationId parseResult))\n            elif not <| String.IsNullOrWhiteSpace(filePath) then\n                if not <| File.Exists(filePath) then\n                    return Error(GraceError.Create $\"File does not exist: {filePath}\" (getCorrelationId parseResult))\n                else\n                    let bytes = File.ReadAllBytes(filePath)\n                    return Ok { Bytes = bytes; MimeType = inferMimeTypeFromFilePath filePath }\n            elif not <| String.IsNullOrWhiteSpace(textInput) then\n                return Ok { Bytes = Encoding.UTF8.GetBytes(textInput); MimeType = \"text/plain\" }\n            else\n                let! stdinText = Console.In.ReadToEndAsync()\n                return Ok { Bytes = Encoding.UTF8.GetBytes(stdinText); MimeType = \"text/plain\" }\n        }\n\n    let private createAndUploadArtifact (graceIds: GraceIds) (artifactType: ArtifactType) (attachmentInput: AttachmentInput) =\n        task {\n            let createParameters =\n                Parameters.Artifact.CreateArtifactParameters(\n                    ArtifactType = getDiscriminatedUnionCaseName artifactType,\n                    MimeType = attachmentInput.MimeType,\n                    Size = int64 attachmentInput.Bytes.LongLength,\n                    Sha256 = computeSha256 attachmentInput.Bytes,\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            match! Artifact.Create(createParameters) with\n            | Error error -> return Error error\n            | Ok createResult ->\n                let createdArtifact = createResult.ReturnValue\n\n                try\n                    do! uploadArtifactContent createdArtifact.UploadUri attachmentInput.Bytes\n                    return Ok createdArtifact.ArtifactId\n                with\n                | ex ->\n                    return\n                        Error(\n                            GraceError.Create\n                                ($\"Failed to upload {getDiscriminatedUnionCaseName artifactType} artifact content: {ex.Message}\")\n                                graceIds.CorrelationId\n                        )\n        }\n\n    let private tryResolveAttachmentType (parseResult: ParseResult) =\n        let attachmentTypeRaw =\n            parseResult.GetValue(Options.attachmentType)\n            |> Option.ofObj\n            |> Option.defaultValue String.Empty\n\n        if String.IsNullOrWhiteSpace attachmentTypeRaw then\n            Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidArtifactType) (getCorrelationId parseResult))\n        else\n            Ok(attachmentTypeRaw.Trim().ToLowerInvariant())\n\n    let private tryResolveOutputFilePath (parseResult: ParseResult) =\n        let outputFileRaw =\n            parseResult.GetValue(Options.outputFile)\n            |> Option.ofObj\n            |> Option.defaultValue String.Empty\n\n        if String.IsNullOrWhiteSpace outputFileRaw then\n            Error(GraceError.Create \"Output file path is required.\" (getCorrelationId parseResult))\n        else\n            try\n                let outputFilePath = Path.GetFullPath(outputFileRaw)\n\n                if Directory.Exists(outputFilePath) then\n                    Error(GraceError.Create $\"Output file path points to a directory: {outputFilePath}\" (getCorrelationId parseResult))\n                else\n                    Ok outputFilePath\n            with\n            | ex -> Error(GraceError.Create $\"Output file path is invalid: {ex.Message}\" (getCorrelationId parseResult))\n\n    let private downloadAttachmentBytes (downloadUri: string) (parseResult: ParseResult) =\n        task {\n            if String.IsNullOrWhiteSpace(downloadUri) then\n                return Error(GraceError.Create \"Attachment download URI was empty.\" (getCorrelationId parseResult))\n            else\n                try\n                    let blobClient = BlobClient(Uri(downloadUri))\n                    let! downloadResult = blobClient.DownloadContentAsync()\n                    return Ok(downloadResult.Value.Content.ToArray())\n                with\n                | ex -> return Error(GraceError.Create ($\"Failed to download attachment bytes: {ex.Message}\") (getCorrelationId parseResult))\n        }\n\n    let private createHandlerImpl (parseResult: ParseResult) =\n        if parseResult |> verbose then printParseResult parseResult\n        let graceIds = parseResult |> getNormalizedIdsAndNames\n\n        let title = parseResult.GetValue(Options.title)\n\n        if String.IsNullOrWhiteSpace title then\n            Task.FromResult(Error(GraceError.Create \"Title is required.\" (getCorrelationId parseResult)))\n        else\n            let description =\n                parseResult.GetValue(Options.description)\n                |> Option.ofObj\n                |> Option.defaultValue String.Empty\n\n            let workItemId =\n                parseResult.GetValue(Options.workItemId)\n                |> Option.ofObj\n                |> Option.defaultValue (Guid.NewGuid().ToString())\n\n            let parameters =\n                Parameters.WorkItem.CreateWorkItemParameters(\n                    WorkItemId = workItemId,\n                    Title = title,\n                    Description = description,\n                    OwnerId = graceIds.OwnerIdString,\n                    OwnerName = graceIds.OwnerName,\n                    OrganizationId = graceIds.OrganizationIdString,\n                    OrganizationName = graceIds.OrganizationName,\n                    RepositoryId = graceIds.RepositoryIdString,\n                    RepositoryName = graceIds.RepositoryName,\n                    CorrelationId = graceIds.CorrelationId\n                )\n\n            if parseResult |> hasOutput then\n                createWorkItemWithProgress parameters\n            else\n                WorkItem.Create(parameters)\n\n    let private createHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! createHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Create() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = createHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private showHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    let parameters =\n                        Parameters.WorkItem.GetWorkItemParameters(\n                            WorkItemId = workItem,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = WorkItem.Get(parameters)\n\n                    match result with\n                    | Ok graceReturnValue ->\n                        if parseResult |> hasOutput then\n                            let jsonText = JsonText(serialize graceReturnValue.ReturnValue)\n                            AnsiConsole.Write(jsonText)\n                            AnsiConsole.WriteLine()\n\n                        return Ok graceReturnValue\n                    | Error error -> return Error error\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Show() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = showHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private statusHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    let statusValue = parseResult.GetValue(Options.statusSet)\n\n                    match discriminatedUnionFromString<WorkItemStatus> statusValue with\n                    | None -> return Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidStatus) (getCorrelationId parseResult))\n                    | Some status ->\n                        let parameters =\n                            Parameters.WorkItem.UpdateWorkItemParameters(\n                                WorkItemId = workItem,\n                                Status = status.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        return! WorkItem.Update(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type Status() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = statusHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private linkReferenceHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n                let referenceIdRaw = parseResult.GetValue(Arguments.referenceId)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    match tryParseGuid referenceIdRaw WorkItemError.InvalidReferenceId parseResult with\n                    | Error error -> return Error error\n                    | Ok referenceId ->\n                        let parameters =\n                            Parameters.WorkItem.LinkReferenceParameters(\n                                WorkItemId = workItem,\n                                ReferenceId = referenceId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        return! WorkItem.LinkReference(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type LinkReference() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = linkReferenceHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private linkPromotionSetHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n                let promotionSetIdRaw = parseResult.GetValue(Arguments.promotionSetId)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    match tryParseGuid promotionSetIdRaw WorkItemError.InvalidPromotionSetId parseResult with\n                    | Error error -> return Error error\n                    | Ok promotionSetId ->\n                        let parameters =\n                            Parameters.WorkItem.LinkPromotionSetParameters(\n                                WorkItemId = workItem,\n                                PromotionSetId = promotionSetId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        return! WorkItem.LinkPromotionSet(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type LinkPromotionSet() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = linkPromotionSetHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private attachHandler (artifactType: ArtifactType) (artifactTypeLabel: string) (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    match! tryGetAttachmentInput parseResult with\n                    | Error error -> return Error error\n                    | Ok attachmentInput ->\n                        match! createAndUploadArtifact graceIds artifactType attachmentInput with\n                        | Error error -> return Error error\n                        | Ok artifactId ->\n                            let linkParameters =\n                                Parameters.WorkItem.LinkArtifactParameters(\n                                    WorkItemId = workItem,\n                                    ArtifactId = artifactId.ToString(),\n                                    OwnerId = graceIds.OwnerIdString,\n                                    OwnerName = graceIds.OwnerName,\n                                    OrganizationId = graceIds.OrganizationIdString,\n                                    OrganizationName = graceIds.OrganizationName,\n                                    RepositoryId = graceIds.RepositoryIdString,\n                                    RepositoryName = graceIds.RepositoryName,\n                                    CorrelationId = graceIds.CorrelationId\n                                )\n\n                            match! WorkItem.LinkArtifact(linkParameters) with\n                            | Error error -> return Error error\n                            | Ok _ ->\n                                let result = { WorkItem = workItem; ArtifactId = artifactId; ArtifactType = artifactTypeLabel }\n\n                                if\n                                    not (parseResult |> json)\n                                    && not (parseResult |> silent)\n                                then\n                                    AnsiConsole.MarkupLine(\n                                        $\"[green]Attached {Markup.Escape(artifactTypeLabel)} content[/] [grey](artifact {Markup.Escape(artifactId.ToString())})[/] [green]to work item[/] {Markup.Escape(workItem)}\"\n                                    )\n\n                                return Ok(GraceReturnValue.Create result graceIds.CorrelationId)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type AttachSummary() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = attachHandler ArtifactType.AgentSummary \"summary\" parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type AttachPrompt() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = attachHandler ArtifactType.Prompt \"prompt\" parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type AttachNotes() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = attachHandler ArtifactType.ReviewNotes \"notes\" parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private writeAttachmentListTable (attachments: Parameters.WorkItem.ListWorkItemAttachmentsResult) =\n        let table = Table(Border = TableBorder.Rounded)\n        table.AddColumn(\"[bold]Artifact ID[/]\") |> ignore\n        table.AddColumn(\"[bold]Type[/]\") |> ignore\n        table.AddColumn(\"[bold]Mime type[/]\") |> ignore\n        table.AddColumn(\"[bold]Size (bytes)[/]\") |> ignore\n        table.AddColumn(\"[bold]Created at[/]\") |> ignore\n\n        let attachmentArray = attachments.Attachments |> Seq.toArray\n        let mutable i = 0\n\n        while i < attachmentArray.Length do\n            let attachment = attachmentArray[i]\n\n            table.AddRow(\n                Markup.Escape(attachment.ArtifactId),\n                Markup.Escape(attachment.AttachmentType),\n                Markup.Escape(attachment.MimeType),\n                attachment.Size.ToString(),\n                Markup.Escape(attachment.CreatedAt)\n            )\n            |> ignore\n\n            i <- i + 1\n\n        AnsiConsole.MarkupLine($\"[bold]Work item ID:[/] {Markup.Escape(attachments.WorkItemId)}\")\n        AnsiConsole.MarkupLine($\"[bold]Work item number:[/] {attachments.WorkItemNumber}\")\n        AnsiConsole.Write(table)\n\n    let private writeShowAttachmentOutput (workItem: string) (showResult: Parameters.WorkItem.ShowWorkItemAttachmentResult) =\n        let selection = if showResult.SelectedUsingLatest then \"latest\" else \"earliest\"\n\n        AnsiConsole.MarkupLine($\"[bold]Work item ID:[/] {Markup.Escape(showResult.WorkItemId)}\")\n        AnsiConsole.MarkupLine($\"[bold]Work item number:[/] {showResult.WorkItemNumber}\")\n        AnsiConsole.MarkupLine($\"[bold]Attachment type:[/] {Markup.Escape(showResult.AttachmentType)}\")\n        AnsiConsole.MarkupLine($\"[bold]Artifact ID:[/] {Markup.Escape(showResult.ArtifactId)}\")\n        AnsiConsole.MarkupLine($\"[bold]Mime type:[/] {Markup.Escape(showResult.MimeType)}\")\n        AnsiConsole.MarkupLine($\"[bold]Size (bytes):[/] {showResult.Size}\")\n        AnsiConsole.MarkupLine($\"[bold]Created at:[/] {Markup.Escape(showResult.CreatedAt)}\")\n        AnsiConsole.MarkupLine($\"[bold]Selection:[/] {selection}\")\n        AnsiConsole.MarkupLine($\"[bold]Available attachments of this type:[/] {showResult.AvailableAttachmentCount}\")\n        AnsiConsole.WriteLine()\n\n        if showResult.IsTextContent then\n            AnsiConsole.MarkupLine(\"[bold]Content:[/]\")\n            Console.WriteLine(showResult.Content)\n        else\n            AnsiConsole.MarkupLine(\"[yellow]Attachment content is binary or non-text and was not rendered inline.[/]\")\n\n            AnsiConsole.MarkupLine(\n                $\"[yellow]Use[/] [bold]grace workitem attachments download {Markup.Escape(workItem)} --artifact-id {Markup.Escape(showResult.ArtifactId)} --output-file <path>[/] [yellow]to save this attachment.[/]\"\n            )\n\n    let private attachmentsListHandlerImpl (parseResult: ParseResult) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n            let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n            match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n            | Error error -> return Error error\n            | Ok workItem ->\n                let parameters =\n                    Parameters.WorkItem.ListWorkItemAttachmentsParameters(\n                        WorkItemId = workItem,\n                        OwnerId = graceIds.OwnerIdString,\n                        OwnerName = graceIds.OwnerName,\n                        OrganizationId = graceIds.OrganizationIdString,\n                        OrganizationName = graceIds.OrganizationName,\n                        RepositoryId = graceIds.RepositoryIdString,\n                        RepositoryName = graceIds.RepositoryName,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                let! result = WorkItem.ListAttachments(parameters)\n\n                match result with\n                | Error error -> return Error error\n                | Ok graceReturnValue ->\n                    if\n                        not (parseResult |> json)\n                        && not (parseResult |> silent)\n                    then\n                        writeAttachmentListTable graceReturnValue.ReturnValue\n\n                    return Ok graceReturnValue\n        }\n\n    let private attachmentsListHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! attachmentsListHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type AttachmentsList() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = attachmentsListHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private attachmentsShowHandlerImpl (parseResult: ParseResult) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n            let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n            match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n            | Error error -> return Error error\n            | Ok workItem ->\n                match tryResolveAttachmentType parseResult with\n                | Error error -> return Error error\n                | Ok attachmentType ->\n                    let latest = parseResult.GetValue(Options.latest)\n\n                    let parameters =\n                        Parameters.WorkItem.ShowWorkItemAttachmentParameters(\n                            WorkItemId = workItem,\n                            AttachmentType = attachmentType,\n                            Latest = latest,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    let! result = WorkItem.ShowAttachment(parameters)\n\n                    match result with\n                    | Error error -> return Error error\n                    | Ok graceReturnValue ->\n                        if\n                            not (parseResult |> json)\n                            && not (parseResult |> silent)\n                        then\n                            writeShowAttachmentOutput workItem graceReturnValue.ReturnValue\n\n                        return Ok graceReturnValue\n        }\n\n    let private attachmentsShowHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! attachmentsShowHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type AttachmentsShow() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = attachmentsShowHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private attachmentsDownloadHandlerImpl (parseResult: ParseResult) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n            let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n            let artifactIdRaw = parseResult.GetValue(Options.artifactId)\n\n            match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n            | Error error -> return Error error\n            | Ok workItem ->\n                match tryParseGuid artifactIdRaw WorkItemError.InvalidArtifactId parseResult with\n                | Error error -> return Error error\n                | Ok artifactId ->\n                    match tryResolveOutputFilePath parseResult with\n                    | Error error -> return Error error\n                    | Ok outputFilePath ->\n                        let parameters =\n                            Parameters.WorkItem.DownloadWorkItemAttachmentParameters(\n                                WorkItemId = workItem,\n                                ArtifactId = artifactId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        match! WorkItem.DownloadAttachment(parameters) with\n                        | Error error -> return Error error\n                        | Ok returnValue ->\n                            match! downloadAttachmentBytes returnValue.ReturnValue.DownloadUri parseResult with\n                            | Error error -> return Error error\n                            | Ok bytes ->\n                                let outputDirectory = Path.GetDirectoryName(outputFilePath)\n\n                                if not (String.IsNullOrWhiteSpace outputDirectory) then\n                                    Directory.CreateDirectory(outputDirectory)\n                                    |> ignore\n\n                                do! File.WriteAllBytesAsync(outputFilePath, bytes)\n\n                                if\n                                    not (parseResult |> json)\n                                    && not (parseResult |> silent)\n                                then\n                                    AnsiConsole.MarkupLine(\n                                        $\"[green]Downloaded[/] {Markup.Escape(returnValue.ReturnValue.AttachmentType)} [green]attachment[/] [grey](artifact {Markup.Escape(returnValue.ReturnValue.ArtifactId)})[/] [green]to[/] {Markup.Escape(outputFilePath)}\"\n                                    )\n\n                                let output =\n                                    {\n                                        WorkItem = workItem\n                                        ArtifactId = artifactId\n                                        AttachmentType = returnValue.ReturnValue.AttachmentType\n                                        OutputFile = outputFilePath\n                                        Size = int64 bytes.LongLength\n                                    }\n\n                                return Ok(GraceReturnValue.Create output graceIds.CorrelationId)\n        }\n\n    let private attachmentsDownloadHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! attachmentsDownloadHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type AttachmentsDownload() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = attachmentsDownloadHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private formatGuidList (values: Guid list) =\n        if values.IsEmpty then\n            \"-\"\n        else\n            values\n            |> List.map (fun value -> value.ToString())\n            |> String.concat Environment.NewLine\n\n    let private writeLinksTable (links: WorkItemLinksDto) =\n        let table = Table(Border = TableBorder.Rounded)\n\n        table.AddColumn(\"[bold]Link category[/]\")\n        |> ignore\n\n        table.AddColumn(\"[bold]Values[/]\") |> ignore\n\n        table.AddRow(\"Work item ID\", Markup.Escape(links.WorkItemId.ToString()))\n        |> ignore\n\n        table.AddRow(\"Work item number\", links.WorkItemNumber.ToString())\n        |> ignore\n\n        table.AddRow(\"References\", Markup.Escape(formatGuidList links.ReferenceIds))\n        |> ignore\n\n        table.AddRow(\"Promotion sets\", Markup.Escape(formatGuidList links.PromotionSetIds))\n        |> ignore\n\n        table.AddRow(\"Summary attachments\", Markup.Escape(formatGuidList links.AgentSummaryArtifactIds))\n        |> ignore\n\n        table.AddRow(\"Prompt attachments\", Markup.Escape(formatGuidList links.PromptArtifactIds))\n        |> ignore\n\n        table.AddRow(\"Notes attachments\", Markup.Escape(formatGuidList links.ReviewNotesArtifactIds))\n        |> ignore\n\n        table.AddRow(\"Other attachments\", Markup.Escape(formatGuidList links.OtherArtifactIds))\n        |> ignore\n\n        AnsiConsole.Write(table)\n\n    let private linksListHandlerImpl (parseResult: ParseResult) =\n        task {\n            if parseResult |> verbose then printParseResult parseResult\n            let graceIds = parseResult |> getNormalizedIdsAndNames\n            let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n            match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n            | Error error -> return Error error\n            | Ok workItem ->\n                let parameters =\n                    Parameters.WorkItem.GetWorkItemLinksParameters(\n                        WorkItemId = workItem,\n                        OwnerId = graceIds.OwnerIdString,\n                        OwnerName = graceIds.OwnerName,\n                        OrganizationId = graceIds.OrganizationIdString,\n                        OrganizationName = graceIds.OrganizationName,\n                        RepositoryId = graceIds.RepositoryIdString,\n                        RepositoryName = graceIds.RepositoryName,\n                        CorrelationId = graceIds.CorrelationId\n                    )\n\n                let! result = WorkItem.GetLinks(parameters)\n\n                match result with\n                | Error error -> return Error error\n                | Ok graceReturnValue ->\n                    if\n                        not (parseResult |> json)\n                        && not (parseResult |> silent)\n                    then\n                        writeLinksTable graceReturnValue.ReturnValue\n\n                    return Ok graceReturnValue\n        }\n\n    let private linksListHandler (parseResult: ParseResult) =\n        task {\n            try\n                return! linksListHandlerImpl parseResult\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type LinksList() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = linksListHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private removeReferenceLinkHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n                let referenceIdRaw = parseResult.GetValue(Arguments.referenceId)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    match tryParseGuid referenceIdRaw WorkItemError.InvalidReferenceId parseResult with\n                    | Error error -> return Error error\n                    | Ok referenceId ->\n                        let parameters =\n                            Parameters.WorkItem.RemoveReferenceLinkParameters(\n                                WorkItemId = workItem,\n                                ReferenceId = referenceId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        return! WorkItem.RemoveReferenceLink(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type RemoveReferenceLink() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = removeReferenceLinkHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private removePromotionSetLinkHandler (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n                let promotionSetIdRaw = parseResult.GetValue(Arguments.promotionSetId)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    match tryParseGuid promotionSetIdRaw WorkItemError.InvalidPromotionSetId parseResult with\n                    | Error error -> return Error error\n                    | Ok promotionSetId ->\n                        let parameters =\n                            Parameters.WorkItem.RemovePromotionSetLinkParameters(\n                                WorkItemId = workItem,\n                                PromotionSetId = promotionSetId.ToString(),\n                                OwnerId = graceIds.OwnerIdString,\n                                OwnerName = graceIds.OwnerName,\n                                OrganizationId = graceIds.OrganizationIdString,\n                                OrganizationName = graceIds.OrganizationName,\n                                RepositoryId = graceIds.RepositoryIdString,\n                                RepositoryName = graceIds.RepositoryName,\n                                CorrelationId = graceIds.CorrelationId\n                            )\n\n                        return! WorkItem.RemovePromotionSetLink(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type RemovePromotionSetLink() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = removePromotionSetLinkHandler parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let private removeArtifactTypeLinksHandler (artifactType: string) (parseResult: ParseResult) =\n        task {\n            try\n                if parseResult |> verbose then printParseResult parseResult\n                let graceIds = parseResult |> getNormalizedIdsAndNames\n                let workItemRaw = parseResult.GetValue(Arguments.workItemIdentifier)\n\n                match tryNormalizeWorkItemIdentifier workItemRaw parseResult with\n                | Error error -> return Error error\n                | Ok workItem ->\n                    let parameters =\n                        Parameters.WorkItem.RemoveArtifactTypeLinksParameters(\n                            WorkItemId = workItem,\n                            ArtifactType = artifactType,\n                            OwnerId = graceIds.OwnerIdString,\n                            OwnerName = graceIds.OwnerName,\n                            OrganizationId = graceIds.OrganizationIdString,\n                            OrganizationName = graceIds.OrganizationName,\n                            RepositoryId = graceIds.RepositoryIdString,\n                            RepositoryName = graceIds.RepositoryName,\n                            CorrelationId = graceIds.CorrelationId\n                        )\n\n                    return! WorkItem.RemoveArtifactTypeLinks(parameters)\n            with\n            | ex -> return Error(GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId parseResult))\n        }\n\n    type RemoveSummaryLinks() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = removeArtifactTypeLinksHandler \"summary\" parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type RemovePromptLinks() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = removeArtifactTypeLinksHandler \"prompt\" parseResult\n                return result |> renderOutput parseResult\n            }\n\n    type RemoveNotesLinks() =\n        inherit AsynchronousCommandLineAction()\n\n        override _.InvokeAsync(parseResult: ParseResult, cancellationToken: CancellationToken) : Task<int> =\n            task {\n                let! result = removeArtifactTypeLinksHandler \"notes\" parseResult\n                return result |> renderOutput parseResult\n            }\n\n    let Build =\n        let addCommonOptions (command: Command) =\n            command\n            |> addOption Options.ownerName\n            |> addOption Options.ownerId\n            |> addOption Options.organizationName\n            |> addOption Options.organizationId\n            |> addOption Options.repositoryName\n            |> addOption Options.repositoryId\n\n        let addAttachInputOptions (command: Command) =\n            command\n            |> addOption Options.file\n            |> addOption Options.text\n            |> addOption Options.stdin\n\n        let workCommand = new Command(\"workitem\", Description = \"Create and manage work items (GUID or positive-number identifiers).\")\n        workCommand.Aliases.Add(\"work\")\n        workCommand.Aliases.Add(\"work-item\")\n        workCommand.Aliases.Add(\"wi\")\n\n        let createCommand =\n            new Command(\"create\", Description = \"Create a new work item.\")\n            |> addOption Options.workItemId\n            |> addOption Options.title\n            |> addOption Options.description\n            |> addCommonOptions\n\n        createCommand.Action <- new Create()\n        workCommand.Subcommands.Add(createCommand)\n\n        let showCommand =\n            new Command(\"show\", Description = \"Show a work item by ID or number.\")\n            |> addCommonOptions\n\n        showCommand.Arguments.Add(Arguments.workItemIdentifier)\n        showCommand.Action <- new Show()\n        workCommand.Subcommands.Add(showCommand)\n\n        let statusCommand =\n            new Command(\"status\", Description = \"Update the status of a work item by ID or number.\")\n            |> addOption Options.statusSet\n            |> addCommonOptions\n\n        statusCommand.Arguments.Add(Arguments.workItemIdentifier)\n        statusCommand.Action <- new Status()\n        workCommand.Subcommands.Add(statusCommand)\n\n        let linkCommand = new Command(\"link\", Description = \"Link related entities to a work item.\")\n\n        let linkRefCommand =\n            new Command(\"ref\", Description = \"Link a reference to a work item.\")\n            |> addCommonOptions\n\n        linkRefCommand.Arguments.Add(Arguments.workItemIdentifier)\n        linkRefCommand.Arguments.Add(Arguments.referenceId)\n        linkRefCommand.Action <- new LinkReference()\n        linkCommand.Subcommands.Add(linkRefCommand)\n\n        let linkPromotionSetCommand =\n            new Command(\"prset\", Description = \"Link a promotion set to a work item.\")\n            |> addCommonOptions\n\n        linkPromotionSetCommand.Arguments.Add(Arguments.workItemIdentifier)\n        linkPromotionSetCommand.Arguments.Add(Arguments.promotionSetId)\n        linkPromotionSetCommand.Action <- new LinkPromotionSet()\n        linkCommand.Subcommands.Add(linkPromotionSetCommand)\n\n        workCommand.Subcommands.Add(linkCommand)\n\n        let attachCommand = new Command(\"attach\", Description = \"Attach summary, prompt, or notes content to a work item.\")\n\n        let attachSummaryCommand =\n            new Command(\"summary\", Description = \"Attach summary content to a work item.\")\n            |> addAttachInputOptions\n            |> addCommonOptions\n\n        attachSummaryCommand.Arguments.Add(Arguments.workItemIdentifier)\n        attachSummaryCommand.Action <- new AttachSummary()\n        attachCommand.Subcommands.Add(attachSummaryCommand)\n\n        let attachPromptCommand =\n            new Command(\"prompt\", Description = \"Attach prompt content to a work item.\")\n            |> addAttachInputOptions\n            |> addCommonOptions\n\n        attachPromptCommand.Arguments.Add(Arguments.workItemIdentifier)\n        attachPromptCommand.Action <- new AttachPrompt()\n        attachCommand.Subcommands.Add(attachPromptCommand)\n\n        let attachNotesCommand =\n            new Command(\"notes\", Description = \"Attach notes content to a work item.\")\n            |> addAttachInputOptions\n            |> addCommonOptions\n\n        attachNotesCommand.Arguments.Add(Arguments.workItemIdentifier)\n        attachNotesCommand.Action <- new AttachNotes()\n        attachCommand.Subcommands.Add(attachNotesCommand)\n\n        workCommand.Subcommands.Add(attachCommand)\n\n        let attachmentsCommand = new Command(\"attachments\", Description = \"List, show, and download reviewer attachments by work item ID or number.\")\n\n        let attachmentsListCommand =\n            new Command(\"list\", Description = \"List summary, prompt, and notes attachments for a work item.\")\n            |> addCommonOptions\n\n        attachmentsListCommand.Arguments.Add(Arguments.workItemIdentifier)\n        attachmentsListCommand.Action <- new AttachmentsList()\n        attachmentsCommand.Subcommands.Add(attachmentsListCommand)\n\n        let attachmentsShowCommand =\n            new Command(\"show\", Description = \"Show one attachment with safe inline text rendering.\")\n            |> addOption Options.attachmentType\n            |> addOption Options.latest\n            |> addCommonOptions\n\n        attachmentsShowCommand.Arguments.Add(Arguments.workItemIdentifier)\n        attachmentsShowCommand.Action <- new AttachmentsShow()\n        attachmentsCommand.Subcommands.Add(attachmentsShowCommand)\n\n        let attachmentsDownloadCommand =\n            new Command(\"download\", Description = \"Download attachment bytes to a local file path.\")\n            |> addOption Options.artifactId\n            |> addOption Options.outputFile\n            |> addCommonOptions\n\n        attachmentsDownloadCommand.Arguments.Add(Arguments.workItemIdentifier)\n        attachmentsDownloadCommand.Action <- new AttachmentsDownload()\n        attachmentsCommand.Subcommands.Add(attachmentsDownloadCommand)\n\n        workCommand.Subcommands.Add(attachmentsCommand)\n\n        let linksCommand = new Command(\"links\", Description = \"Inspect and remove work item links.\")\n\n        let linksListCommand =\n            new Command(\"list\", Description = \"List current links for a work item.\")\n            |> addCommonOptions\n\n        linksListCommand.Arguments.Add(Arguments.workItemIdentifier)\n        linksListCommand.Action <- new LinksList()\n        linksCommand.Subcommands.Add(linksListCommand)\n\n        let linksRemoveCommand = new Command(\"remove\", Description = \"Remove one or more links from a work item.\")\n\n        let removeReferenceCommand =\n            new Command(\"ref\", Description = \"Remove a reference link from a work item.\")\n            |> addCommonOptions\n\n        removeReferenceCommand.Arguments.Add(Arguments.workItemIdentifier)\n        removeReferenceCommand.Arguments.Add(Arguments.referenceId)\n        removeReferenceCommand.Action <- new RemoveReferenceLink()\n        linksRemoveCommand.Subcommands.Add(removeReferenceCommand)\n\n        let removePromotionSetCommand =\n            new Command(\"prset\", Description = \"Remove a promotion set link from a work item.\")\n            |> addCommonOptions\n\n        removePromotionSetCommand.Arguments.Add(Arguments.workItemIdentifier)\n        removePromotionSetCommand.Arguments.Add(Arguments.promotionSetId)\n        removePromotionSetCommand.Action <- new RemovePromotionSetLink()\n        linksRemoveCommand.Subcommands.Add(removePromotionSetCommand)\n\n        let removeSummaryLinksCommand =\n            new Command(\"summary\", Description = \"Remove all summary attachments from a work item.\")\n            |> addCommonOptions\n\n        removeSummaryLinksCommand.Arguments.Add(Arguments.workItemIdentifier)\n        removeSummaryLinksCommand.Action <- new RemoveSummaryLinks()\n        linksRemoveCommand.Subcommands.Add(removeSummaryLinksCommand)\n\n        let removePromptLinksCommand =\n            new Command(\"prompt\", Description = \"Remove all prompt attachments from a work item.\")\n            |> addCommonOptions\n\n        removePromptLinksCommand.Arguments.Add(Arguments.workItemIdentifier)\n        removePromptLinksCommand.Action <- new RemovePromptLinks()\n        linksRemoveCommand.Subcommands.Add(removePromptLinksCommand)\n\n        let removeNotesLinksCommand =\n            new Command(\"notes\", Description = \"Remove all notes attachments from a work item.\")\n            |> addCommonOptions\n\n        removeNotesLinksCommand.Arguments.Add(Arguments.workItemIdentifier)\n        removeNotesLinksCommand.Action <- new RemoveNotesLinks()\n        linksRemoveCommand.Subcommands.Add(removeNotesLinksCommand)\n\n        linksCommand.Subcommands.Add(linksRemoveCommand)\n        workCommand.Subcommands.Add(linksCommand)\n\n        workCommand\n"
  },
  {
    "path": "src/Grace.CLI/Conversion.md",
    "content": "# Grace CLI Command Conversion Guide\n\nThis note bundles the shared knowledge needed to migrate legacy CLI subcommands from `CommandHandler.Create` to the `AsynchronousCommandLineAction` pattern. Use it to plan batches, avoid re-scanning the same F# sources, and keep future prompts short.\n\n---\n\n## 1. Core Pattern\n\n- **Entry point**: Replace each `CommandHandler.Create` assignment with `command.Action <- new Xxx()` where `Xxx` is a new action class in the same module.\n- **Class template**:\n\n  ```fsharp\n  /// Module.CommandName subcommand definition\n  type CommandName() =\n      inherit AsynchronousCommandLineAction()\n\n      override _.InvokeAsync(parseResult: ParseResult, ct: CancellationToken) : Tasks.Task<int> =\n          task {\n              try\n                  if parseResult |> verbose then printParseResult parseResult\n                  let graceIds = parseResult |> getNormalizedIdsAndNames\n                  let validateIncomingParameters = parseResult |> CommonValidations\n\n                  match validateIncomingParameters with\n                  | Ok _ ->\n                      let parameters =\n                          Parameters.Namespace.SomeSdkParameters(\n                              // map IDs and options here\n                          )\n\n                      if parseResult |> hasOutput then\n                          let! result =\n                              progress\n                                  .Columns(progressColumns)\n                                  .StartAsync(fun progressContext ->\n                                      task {\n                                          let t0 =\n                                              progressContext.AddTask($\"[{Color.DodgerBlue1}]Sending command to the server.[/]\")\n                                          let! response = SdkModule.Operation(parameters)\n                                          t0.Increment(100.0)\n                                          return response\n                                      })\n\n                          return result |> renderOutput parseResult\n                      else\n                          let! result = SdkModule.Operation(parameters)\n                          return result |> renderOutput parseResult\n                  | Error error ->\n                      return (Error error) |> renderOutput parseResult\n              with ex ->\n                  return\n                      renderOutput\n                          parseResult\n                          (GraceResult.Error(GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (parseResult |> getCorrelationId)))\n          }\n  ```\n\n- **Validation & normalization**:\n  - `let graceIds = parseResult |> getNormalizedIdsAndNames`\n  - `let validateIncomingParameters = parseResult |> CommonValidations`\n  - When existing helpers already normalize parameters, reuse them, but remove obsolete `XxxParameters` types and private handler functions.\n- **SDK parameter builders**: Map from `graceIds.*` and `parseResult.GetValue Options.xxx`. Keep correlation IDs: `CorrelationId = getCorrelationId parseResult`.\n- **Progress UI**: Preserve any existing `progress.Columns(progressColumns)` blocks and nested tasks exactly; only wrap them with `let! result = ...` and `return result |> renderOutput parseResult`.\n- **Output**: Always render using `result |> renderOutput parseResult`. When the old code updated configuration (e.g., `Organization.Create`), keep that logic inside the success branch before returning.\n- **Error handling**: Use the `with ex ->` clause shown above to wrap in `GraceResult.Error`.\n- **Build hygiene**: Each file should compile independently. Run `dotnet build --configuration Release` after a batch rather than after every command unless you suspect a break in the current file.\n\n---\n\n## 2. Suggested Working Cadence\n\n1. Choose a module (e.g., `Repository.CLI.fs`) and convert 2–4 commands per prompt iteration.\n2. After finishing a batch, list which handlers are done and which remain; skip per-command “ready checks” to save tokens.\n3. Run `dotnet build --configuration Release` when the current file is green. Report only compile issues tied to the edited file.\n4. When pausing, note the next handler you intend to convert to resume smoothly without re-reading the module.\n\n---\n\n## 3. Module Cheat Sheets\n\nUse these summaries to map options to SDK parameters quickly.\n\n### Repository (`Grace.CLI/Command/Repository.CLI.fs`)\n\n| Command | SDK target | Key options / notes |\n| --- | --- | --- |\n| `Create` | `Repository.Create` | Uses org and owner IDs; mirrors existing config update logic. |\n| `Init` | `Repository.Init` | accepts `--graceConfig`; may read defaults from file system. |\n| `Get` / `GetBranches` | `Repository.Get*` | Expect pagination flags (check existing handler). |\n| `SetVisibility` | `Repository.SetVisibility` | `Options.visibility` maps to `RepositoryType`. |\n| `SetStatus` | `Repository.SetStatus` | `--status` from `RepositoryStatus`. |\n| `SetRecordSaves` | `Repository.SetRecordSaves` | Boolean `--recordSaves`. |\n| `SetSaveDays` / `SetCheckpointDays` / `SetDiffCacheDays` / `SetDirectoryVersionCacheDays` / `SetLogicalDeleteDays` | corresponding storage parameter types | Single-precision floats from options (keep cast). |\n| `SetDefaultServerApiVersion` | `Repository.SetDefaultServerApiVersion` | `--defaultServerApiVersion`. |\n| `SetName` / `SetDescription` | `Repository.SetName`, `Repository.SetDescription` | `OptionName.NewName` / `OptionName.Description`. |\n| `Delete` / `Undelete` | `Repository.Delete` / `Repository.Undelete` | Include `--deleteReason` and `--force` flags as applicable. |\n| `SetAnonymousAccess`, `SetAllowsLargeFiles` | `Repository.SetAnonymousAccess`, `Repository.SetAllowsLargeFiles` | Boolean toggles; preserve output text. |\n\nSpecial considerations:\n- Many commands rely on the shared `graceIds.RepositoryIdString`. When options are optional, fetch `graceIds` first and rely on defaults set in parsing logic.\n- Some handlers adjust configuration or display multi-step progress; keep any post-call mutations (e.g., updating `Current()`).\n\n### Owner (`Grace.CLI/Command/Owner.CLI.fs`)\n\n| Command | Notes |\n| --- | --- |\n| `Create`, `Get` | Similar to Organization pattern; ensure new owner IDs fall back to defaults when implicit. |\n| `SetName`, `SetType`, `SetSearchVisibility`, `SetDescription` | Mirror `Organization.SetType` example for structure; options map to string enums validated via `.AcceptOnlyFromAmong`. |\n| `Delete`, `Undelete` | Include `--deleteReason` / `--force` handling if present. |\n\n### Branch (`Grace.CLI/Command/Branch.CLI.fs`)\n\n- Command list: `Create`, `Switch`, `Status`, `Promote`, `Commit`, `Checkpoint`, `Save`, `Tag`, `CreateExternal`, `Rebase`, `ListContents`, `GetRecursiveSize`, `Get` (and event wrappers), `GetReferences`, `GetPromotions`, `GetCommits`, `GetCheckpoints`, `GetSaves`, `GetTags`, `GetExternals`, `Assign`, `SetName`, `Delete`, plus any feature toggles still using `CommandHandler.Create`.\n- Highlights:\n  - Many handlers compose multiple SDK calls with shared progress tasks; reproduce nested tasks exactly.\n  - Option modules contain GUID validation using `validateGuid`; keep parse-time validation as-is.\n  - Several commands stream console output (`Console.WriteLine`, `AnsiConsole.Write`) in addition to returning results; leave those statements untouched.\n  - `Switch`/`Create` update local configuration; ensure config updates stay before returning.\n\n### Reference (`Grace.CLI/Command/Reference.CLI.fs`)\n\n- Commands mirror branch operations (`Promote`, `Commit`, `Checkpoint`, `Save`, `Tag`, `CreateExternal`, `Get`, `Delete`, `Assign`).\n- Shares many options via `Branch` helper modules; focus on using `graceIds` for repository context and explicit branch/reference IDs supplied via options.\n\n### Maintenance (`Grace.CLI/Command/Maintenance.CLI.fs`)\n\n- Commands: `Test`, `UpdateIndex`, `Scan`, `Stats`, `ListContents`.\n- Usually take only common parameters plus optional filters.\n- Each handler may print structured diagnostics; keep logging intact.\n- `ListContents` uses a custom `ListContentsParameters`; delete the type after inlining option reads.\n\n### DirectoryVersion (`Grace.CLI/Command/DirectoryVersion.CLI.fs`)\n\n- All current `CommandHandler.Create` wrappers (`Get`, `Save`, `GetZipFile`, etc.) must convert.\n- Parameters often rely on directory paths and recursion flags; map options to `Grace.Shared.Parameters.DirectoryVersion` builders.\n- Some commands interact with file system (e.g., writing zip). Keep `use` bindings and `Directory.CreateDirectory` calls.\n\n---\n\n## 4. Tracking Progress\n\n- Use `rg \"CommandHandler.Create\" Grace.CLI/Command` to confirm remaining legacy handlers.\n- Maintain a simple checklist (e.g., in your prompt or local notes) marking each handler as converted. Reference this doc instead of re-opening every file.\n- When resuming work, note which `command.Action` assignments are still old-style; they provide a quick diff target.\n\n---\n\n## 5. Verification\n\n- After finishing each file:\n  - `dotnet build --configuration Release` (captures errors across the solution).\n  - Optionally run targeted tests if the module has coverage (`dotnet test --no-build --filter FullyQualifiedName~Grace.CLI`).\n- Ensure Fantomas formatting if required: `dotnet tool run fantomas Grace.CLI/Command/<File>.fs`.\n\n---\n\nBy sticking to this reference and working in batches, you can minimize token usage per iteration while migrating all CLI subcommands to the new action pattern. Keep this document open during conversion sessions and update it if new patterns emerge (e.g., additional validation helpers or SDK parameter changes).\n\n"
  },
  {
    "path": "src/Grace.CLI/Grace.CLI.fsproj",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project Sdk=\"Microsoft.NET.Sdk\">\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<LangVersion>preview</LangVersion>\n\t\t<PublishReadyToRun>true</PublishReadyToRun>\n\t\t<OutputType>Exe</OutputType>\n\t\t<!--<PublishAot>true</PublishAot>-->\n\t\t<!--<RuntimeIdentifier>win10-x64</RuntimeIdentifier>-->\n\t\t<Version>0.1</Version>\n\t\t<Description>The command-line interface for Grace.</Description>\n\t\t<GenerateDocumentationFile>true</GenerateDocumentationFile>\n\t\t<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <WarningsAsErrors>FS0025</WarningsAsErrors>\n\t\t<NoWarn>67;1057,3391</NoWarn>\n\t\t<Platforms>AnyCPU</Platforms>\n\t\t<FSharpPreferNetFrameworkTools>False</FSharpPreferNetFrameworkTools>\n\t\t<OtherFlags>--test:GraphBasedChecking</OtherFlags>\n\t\t<OtherFlags>--test:ParallelOptimization</OtherFlags>\n\t\t<OtherFlags>--test:ParallelIlxGen</OtherFlags>\n\t\t<AssemblyTitle>Grace Version Control System CLI</AssemblyTitle>\n\t\t<AssemblyName>grace</AssemblyName>\n\t</PropertyGroup>\n\t<ItemGroup>\n\t\t<None Include=\"instructions.md\" />\n\t\t<Compile Include=\"Log.CLI.fs\" />\n\t\t<Compile Include=\"Text.CLI.fs\" />\n\t\t<Compile Include=\"LocalStateDb.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Services.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Common.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Auth.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Config.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Owner.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Connect.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Organization.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Repository.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Diff.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Branch.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Reference.CLI.fs\" />\n\t\t<Compile Include=\"Command\\DirectoryVersion.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Watch.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Maintenance.CLI.fs\" />\n                \n                <Compile Include=\"Command\\WorkItem.CLI.fs\" />\n                <Compile Include=\"Command\\Review.CLI.fs\" />\n                <Compile Include=\"Command\\Candidate.CLI.fs\" />\n                <Compile Include=\"Command\\Queue.CLI.fs\" />\n                <Compile Include=\"Command\\PromotionSet.CLI.fs\" />\n                <Compile Include=\"Command\\Agent.CLI.fs\" />\n                <Compile Include=\"Command\\Access.CLI.fs\" />\n\t\t<Compile Include=\"Command\\Admin.CLI.fs\" />\n\t\t<Compile Include=\"HistoryStorage.CLI.fs\" />\n\t\t<Compile Include=\"Command\\History.CLI.fs\" />\n\t\t<Compile Include=\"Program.CLI.fs\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n\t\t<PackageReference Include=\"Microsoft.Data.Sqlite.Core\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"SQLitePCLRaw.bundle_e_sqlite3\" Version=\"2.1.11\" />\n\t\t<PackageReference Include=\"MessagePack\" Version=\"3.1.4\" />\n\t\t<PackageReference Include=\"MessagePack.Annotations\" Version=\"3.1.4\" />\n\t\t<PackageReference Include=\"MessagePack.FSharpExtensions\" Version=\"4.0.0\" />\n\t\t<PackageReference Include=\"MessagePack.NodaTime\" Version=\"3.5.0\" />\n\t\t<PackageReference Include=\"Microsoft.AspNetCore.SignalR.Client\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.AspNetCore.SignalR.Protocols.Json\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Logging.Configuration\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.Extensions.Logging.Console\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.Identity.Client\" Version=\"4.79.2\" />\n\t\t<PackageReference Include=\"Microsoft.Identity.Client.Extensions.Msal\" Version=\"4.79.2\" />\n\t\t<PackageReference Include=\"NodaTime\" Version=\"3.2.2\" />\n\t\t<PackageReference Include=\"NodaTime.Serialization.SystemTextJson\" Version=\"1.3.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Api\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.Console\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.Zipkin\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"Polly\" Version=\"8.6.5\" />\n\t\t<PackageReference Include=\"Spectre.Console\" Version=\"0.54.0\" />\n\t\t<PackageReference Include=\"Spectre.Console.ImageSharp\" Version=\"0.54.0\" />\n\t\t<PackageReference Include=\"Spectre.Console.Json\" Version=\"0.54.0\" />\n\t\t<PackageReference Include=\"System.CommandLine\" Version=\"2.0.0\" />\n\t\t<PackageReference Include=\"System.Reactive\" Version=\"6.1.0\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\Grace.SDK\\Grace.SDK.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n\t</ItemGroup>\n  <ItemGroup>\n    <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n  </ItemGroup>\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Grace.CLI.Tests\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.CLI/HistoryStorage.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen Grace.CLI.Text\nopen Grace.Shared\nopen Grace.Shared.Client\nopen Grace.Shared.Utilities\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.Reflection\nopen System.Text\nopen System.Text.Json\nopen System.Text.RegularExpressions\nopen System.Threading\n\nmodule HistoryStorage =\n\n    [<Literal>]\n    let Placeholder = \"__REDACTED__\"\n\n    let private jsonlOptions =\n        let options = JsonSerializerOptions(Constants.JsonSerializerOptions)\n        options.WriteIndented <- false\n        options\n\n    type Redaction = { kind: string; name: string; argIndex: int; originalLength: int option; placeholder: string }\n\n    type HistoryEntry =\n        {\n            id: Guid\n            timestampUtc: Instant\n            argvOriginal: string array\n            argvNormalized: string array\n            commandLine: string\n            cwd: string\n            repoRoot: string option\n            repoName: string option\n            repoBranch: string option\n            graceVersion: string\n            exitCode: int\n            durationMs: int64\n            parseSucceeded: bool\n            redactions: Redaction list\n            source: string option\n        }\n\n    type ReadResult = { Entries: HistoryEntry list; CorruptCount: int }\n\n    type RecordInput =\n        {\n            argvOriginal: string array\n            argvNormalized: string array\n            cwd: string\n            exitCode: int\n            durationMs: int64\n            parseSucceeded: bool\n            timestampUtc: Instant\n            source: string option\n        }\n\n    let private lockBackoffMs =\n        [|\n            25\n            50\n            100\n            150\n            200\n            250\n            300\n            400\n            500\n            750\n        |]\n\n    let getHistoryFilePath () =\n        let userGraceDir = UserConfiguration.getUserGraceDirectory ()\n        Path.Combine(userGraceDir, \"history.jsonl\")\n\n    let getHistoryLockPath () =\n        let userGraceDir = UserConfiguration.getUserGraceDirectory ()\n        Path.Combine(userGraceDir, \"history.lock\")\n\n    let private ensureHistoryDirectory () =\n        UserConfiguration.ensureUserGraceDirectory ()\n        |> ignore\n\n    let private getGraceVersion () =\n        try\n            let version = Assembly.GetEntryAssembly().GetName().Version\n\n            if isNull version then\n                Constants.CurrentConfigurationVersion\n            else\n                version.ToString()\n        with\n        | _ -> Constants.CurrentConfigurationVersion\n\n    let private tryAcquireLock () =\n        ensureHistoryDirectory ()\n        let lockPath = getHistoryLockPath ()\n        let mutable acquired: FileStream option = None\n\n        for attempt in 0 .. lockBackoffMs.Length - 1 do\n            if acquired.IsNone then\n                try\n                    let stream = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)\n\n                    acquired <- Some stream\n                with\n                | :? IOException -> Thread.Sleep(lockBackoffMs[attempt])\n\n        acquired\n\n    let private withHistoryLock (onLocked: unit -> 'T) (onFailure: unit -> 'T) =\n        match tryAcquireLock () with\n        | Some stream ->\n            try\n                onLocked ()\n            finally\n                stream.Dispose()\n        | None -> onFailure ()\n\n    let private quoteArg (arg: string) =\n        if String.IsNullOrEmpty(arg) then\n            \"\\\"\\\"\"\n        elif arg.IndexOfAny([| ' '; '\\t'; '\"' |]) >= 0 then\n            \"\\\"\" + arg.Replace(\"\\\"\", \"\\\\\\\"\") + \"\\\"\"\n        else\n            arg\n\n    let buildCommandLine (argv: string array) = argv |> Array.map quoteArg |> String.concat \" \"\n\n    let tryFindRepoRoot (startDirectory: string) =\n        try\n            let mutable current = DirectoryInfo(startDirectory)\n            let mutable found: string option = None\n\n            while (not <| isNull current) && found.IsNone do\n                let configPath = Path.Combine(current.FullName, Constants.GraceConfigDirectory, Constants.GraceConfigFileName)\n\n                if File.Exists(configPath) then\n                    found <- Some current.FullName\n                else\n                    current <- current.Parent\n\n            found\n        with\n        | _ -> None\n\n    let tryParseDuration (value: string) =\n        if String.IsNullOrWhiteSpace(value) then\n            Error \"Duration cannot be empty.\"\n        else\n            let trimmed = value.Trim()\n            let suffix = trimmed[trimmed.Length - 1]\n            let numberPart = trimmed.Substring(0, trimmed.Length - 1)\n\n            match Double.TryParse(numberPart) with\n            | true, amount ->\n                match suffix with\n                | 's' -> Ok(Duration.FromSeconds(amount))\n                | 'm' -> Ok(Duration.FromMinutes(amount))\n                | 'h' -> Ok(Duration.FromHours(amount))\n                | 'd' -> Ok(Duration.FromDays(amount))\n                | _ -> Error \"Duration must end with s, m, h, or d.\"\n            | _ -> Error \"Duration must be a number followed by s, m, h, or d.\"\n\n    let private tryGetRepoName (repoRoot: string option) =\n        match repoRoot with\n        | Some root when not <| String.IsNullOrWhiteSpace(root) ->\n            try\n                let name = DirectoryInfo(root).Name\n                if String.IsNullOrWhiteSpace(name) then None else Some name\n            with\n            | _ -> None\n        | _ -> None\n\n    let private hasGitMetadata (repoRoot: string) =\n        let gitPath = Path.Combine(repoRoot, \".git\")\n        Directory.Exists(gitPath) || File.Exists(gitPath)\n\n    let private tryGetGitBranch (repoRoot: string option) =\n        match repoRoot with\n        | Some root when\n            not <| String.IsNullOrWhiteSpace(root)\n            && hasGitMetadata root\n            ->\n            try\n                let startInfo = ProcessStartInfo()\n                startInfo.FileName <- \"git\"\n                startInfo.Arguments <- \"rev-parse --abbrev-ref HEAD\"\n                startInfo.WorkingDirectory <- root\n                startInfo.RedirectStandardOutput <- true\n                startInfo.RedirectStandardError <- true\n                startInfo.UseShellExecute <- false\n                startInfo.CreateNoWindow <- true\n\n                use proc = new Process()\n                proc.StartInfo <- startInfo\n\n                if proc.Start() then\n                    if proc.WaitForExit(2000) then\n                        let output = proc.StandardOutput.ReadToEnd().Trim()\n\n                        if\n                            proc.ExitCode = 0\n                            && not <| String.IsNullOrWhiteSpace(output)\n                            && not (output.Equals(\"HEAD\", StringComparison.OrdinalIgnoreCase))\n                        then\n                            Some output\n                        else\n                            None\n                    else\n                        try\n                            proc.Kill(true)\n                        with\n                        | _ -> ()\n\n                        None\n                else\n                    None\n            with\n            | _ -> None\n        | _ -> None\n\n    let private buildSensitiveOptionSet (historyConfig: UserConfiguration.HistoryConfiguration) =\n        let names = HashSet<string>(StringComparer.InvariantCultureIgnoreCase)\n\n        for name in (UserConfiguration.defaultRedactOptionNames ()) do\n            names.Add(name) |> ignore\n\n        if not <| isNull historyConfig.RedactOptionNames then\n            for name in historyConfig.RedactOptionNames do\n                if not <| String.IsNullOrWhiteSpace(name) then names.Add(name.Trim()) |> ignore\n\n        names\n\n    let private buildRegexes (patterns: string array) =\n        let regexes = ResizeArray<Regex>()\n\n        if not <| isNull patterns then\n            for pattern in patterns do\n                if not <| String.IsNullOrWhiteSpace(pattern) then\n                    try\n                        regexes.Add(\n                            Regex(\n                                pattern,\n                                RegexOptions.Compiled\n                                ||| RegexOptions.CultureInvariant,\n                                TimeSpan.FromSeconds(1.0)\n                            )\n                        )\n                    with\n                    | _ -> ()\n\n        regexes |> Seq.toList\n\n    let private redactOptions (args: string array) (sensitiveOptions: HashSet<string>) =\n        let redactions = ResizeArray<Redaction>()\n        let redacted = Array.copy args\n\n        let mutable i = 0\n\n        while i < redacted.Length do\n            let current = redacted[i]\n\n            if\n                not <| String.IsNullOrWhiteSpace(current)\n                && current.StartsWith(\"--\")\n            then\n                let optionPart = current.Substring(2)\n                let equalsIndex = optionPart.IndexOf('=')\n\n                if equalsIndex >= 0 then\n                    let optionName = optionPart.Substring(0, equalsIndex)\n                    let optionValue = optionPart.Substring(equalsIndex + 1)\n\n                    if sensitiveOptions.Contains(optionName) then\n                        redacted[i] <- $\"--{optionName}={Placeholder}\"\n\n                        redactions.Add(\n                            { kind = \"OptionValue\"; name = optionName; argIndex = i; originalLength = Some optionValue.Length; placeholder = Placeholder }\n                        )\n                else\n                    let optionName = optionPart\n\n                    if sensitiveOptions.Contains(optionName) then\n                        if i + 1 < redacted.Length then\n                            let optionValue = redacted[i + 1]\n                            redacted[i + 1] <- Placeholder\n\n                            redactions.Add(\n                                {\n                                    kind = \"OptionValue\"\n                                    name = optionName\n                                    argIndex = i + 1\n                                    originalLength = Some optionValue.Length\n                                    placeholder = Placeholder\n                                }\n                            )\n\n            i <- i + 1\n\n        redacted, redactions |> Seq.toList\n\n    let private applyRegexRedactions (args: string array) (regexes: Regex list) =\n        let redactions = ResizeArray<Redaction>()\n        let redacted = Array.copy args\n\n        for i in 0 .. redacted.Length - 1 do\n            let mutable updated = redacted[i]\n\n            for regex in regexes do\n                if not <| String.IsNullOrWhiteSpace(updated) then\n                    let matches = regex.Matches(updated)\n\n                    if matches.Count > 0 then\n                        for m in matches do\n                            let prefix = if m.Groups.Count > 1 then m.Groups[1].Value else String.Empty\n\n                            let sensitiveLength =\n                                if m.Groups.Count > 1 then\n                                    Math.Max(0, m.Value.Length - prefix.Length)\n                                else\n                                    m.Value.Length\n\n                            redactions.Add(\n                                { kind = \"RegexMatch\"; name = regex.ToString(); argIndex = i; originalLength = Some sensitiveLength; placeholder = Placeholder }\n                            )\n\n                        updated <- regex.Replace(updated, (fun (m: Match) -> if m.Groups.Count > 1 then m.Groups[1].Value + Placeholder else Placeholder))\n\n            redacted[i] <- updated\n\n        redacted, redactions |> Seq.toList\n\n    let redactArguments (args: string array) (historyConfig: UserConfiguration.HistoryConfiguration) =\n        if isNull args then\n            Array.empty, List.empty\n        else\n            let sensitiveOptions = buildSensitiveOptionSet historyConfig\n            let regexes = buildRegexes historyConfig.RedactRegexes\n\n            let redactedAfterOptions, optionRedactions = redactOptions args sensitiveOptions\n            let fullyRedacted, regexRedactions = applyRegexRedactions redactedAfterOptions regexes\n            fullyRedacted, (optionRedactions @ regexRedactions)\n\n    let readHistoryEntries () =\n        ensureHistoryDirectory ()\n        let path = getHistoryFilePath ()\n\n        if not <| File.Exists(path) then\n            { Entries = List.empty; CorruptCount = 0 }\n        else\n            let mutable attempts = 0\n            let mutable success = false\n            let mutable result = { Entries = List.empty; CorruptCount = 0 }\n\n            while attempts < lockBackoffMs.Length && not success do\n                try\n                    let entries = ResizeArray<HistoryEntry>()\n                    let mutable corrupt = 0\n\n                    use stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)\n                    use reader = new StreamReader(stream, Encoding.UTF8)\n                    let mutable line = reader.ReadLine()\n\n                    while not <| isNull line do\n                        if not <| String.IsNullOrWhiteSpace(line) then\n                            try\n                                let entry = JsonSerializer.Deserialize<HistoryEntry>(line, Constants.JsonSerializerOptions)\n\n                                if obj.ReferenceEquals(entry, null) then\n                                    corrupt <- corrupt + 1\n                                else\n                                    entries.Add(entry)\n                            with\n                            | _ -> corrupt <- corrupt + 1\n\n                        line <- reader.ReadLine()\n\n                    result <- { Entries = entries |> Seq.toList; CorruptCount = corrupt }\n                    success <- true\n                with\n                | :? IOException ->\n                    Thread.Sleep(lockBackoffMs[attempts])\n                    attempts <- attempts + 1\n\n            result\n\n    let private writeHistoryEntries (entries: HistoryEntry list) =\n        ensureHistoryDirectory ()\n        let historyPath = getHistoryFilePath ()\n        let tempPath = historyPath + \".tmp\"\n        let backupPath = historyPath + \".bak\"\n\n        let tryDeleteFile (path: string) =\n            let mutable attempts = 0\n            let mutable deleted = false\n\n            while attempts < lockBackoffMs.Length && not deleted do\n                try\n                    if File.Exists(path) then File.Delete(path)\n                    deleted <- true\n                with\n                | :? IOException\n                | :? UnauthorizedAccessException ->\n                    Thread.Sleep(lockBackoffMs[attempts])\n                    attempts <- attempts + 1\n\n        do\n            use stream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)\n            use writer = new StreamWriter(stream, Encoding.UTF8)\n\n            for entry in entries do\n                let json = JsonSerializer.Serialize(entry, jsonlOptions)\n                writer.WriteLine(json)\n\n            writer.Flush()\n            stream.Flush(true)\n\n        let mutable attempts = 0\n        let mutable replaced = false\n\n        while attempts < lockBackoffMs.Length && not replaced do\n            try\n                if File.Exists(historyPath) then\n                    try\n                        File.Replace(tempPath, historyPath, backupPath, true)\n                        tryDeleteFile backupPath\n                    with\n                    | :? IOException -> File.Move(tempPath, historyPath, true)\n                else\n                    File.Move(tempPath, historyPath)\n\n                replaced <- true\n            with\n            | :? IOException\n            | :? UnauthorizedAccessException ->\n                Thread.Sleep(lockBackoffMs[attempts])\n                attempts <- attempts + 1\n\n        if not replaced then tryDeleteFile tempPath\n\n    let private pruneIfNeeded (historyConfig: UserConfiguration.HistoryConfiguration) =\n        let historyPath = getHistoryFilePath ()\n        let fileInfo = FileInfo(historyPath)\n\n        let readResult = readHistoryEntries ()\n        let entries = readResult.Entries\n\n        let retentionCutoff =\n            if historyConfig.RetentionDays > 0 then\n                Some(\n                    getCurrentInstant()\n                        .Minus(Duration.FromDays(float historyConfig.RetentionDays))\n                )\n            else\n                None\n\n        let retained =\n            match retentionCutoff with\n            | Some cutoff ->\n                entries\n                |> List.filter (fun entry -> entry.timestampUtc >= cutoff)\n            | None -> entries\n\n        let trimmed =\n            if historyConfig.MaxEntries > 0\n               && retained.Length > historyConfig.MaxEntries then\n                retained\n                |> List.sortByDescending (fun entry -> entry.timestampUtc)\n                |> List.truncate historyConfig.MaxEntries\n            else\n                retained\n\n        let trimmedOrdered =\n            trimmed\n            |> List.sortBy (fun entry -> entry.timestampUtc)\n\n        let exceedsSize =\n            if historyConfig.MaxFileBytes > 0L then\n                fileInfo.Exists\n                && fileInfo.Length > historyConfig.MaxFileBytes\n            else\n                false\n\n        let exceedsCount =\n            historyConfig.MaxEntries > 0\n            && entries.Length > historyConfig.MaxEntries\n\n        let exceedsRetention = retained.Length <> entries.Length\n\n        if exceedsSize || exceedsCount || exceedsRetention then\n            writeHistoryEntries trimmedOrdered\n\n        readResult\n\n    let private appendHistoryEntry (entry: HistoryEntry) (historyConfig: UserConfiguration.HistoryConfiguration) =\n        ensureHistoryDirectory ()\n        let historyPath = getHistoryFilePath ()\n        let json = JsonSerializer.Serialize(entry, jsonlOptions)\n\n        do\n            use stream = new FileStream(historyPath, FileMode.Append, FileAccess.Write, FileShare.Read)\n            use writer = new StreamWriter(stream, Encoding.UTF8)\n            writer.WriteLine(json)\n            writer.Flush()\n            stream.Flush(true)\n\n        pruneIfNeeded historyConfig |> ignore\n\n    let private tryGetTopLevelCommand (tokens: string array) =\n        if isNull tokens || tokens.Length = 0 then\n            None\n        else\n            let comparison =\n                if runningOnWindows then\n                    StringComparison.InvariantCultureIgnoreCase\n                else\n                    StringComparison.InvariantCulture\n\n            let isOptionWithValue (token: string) =\n                token.Equals(OptionName.Output, comparison)\n                || token.Equals(\"-o\", comparison)\n                || token.Equals(OptionName.CorrelationId, comparison)\n                || token.Equals(\"-c\", comparison)\n                || token.Equals(OptionName.Source, comparison)\n\n            let rec loop index =\n                if index >= tokens.Length then\n                    None\n                else\n                    let token = tokens[index]\n\n                    if token = \"--\" then\n                        if index + 1 < tokens.Length then Some tokens[index + 1] else None\n                    elif token.StartsWith(\"-\", StringComparison.Ordinal) then\n                        let nextIndex = if isOptionWithValue token then index + 2 else index + 1\n                        loop nextIndex\n                    else\n                        Some token\n\n            loop 0\n\n    let private normalizeSourceOption (value: string option) =\n        value\n        |> Option.bind (fun source -> if String.IsNullOrWhiteSpace(source) then None else Some(source.Trim()))\n\n    let shouldRecord (input: RecordInput) (historyConfig: UserConfiguration.HistoryConfiguration) =\n        if not historyConfig.Enabled then\n            false\n        else\n            let tokens = if isNull input.argvNormalized then Array.empty else input.argvNormalized\n\n            let commandName =\n                tryGetTopLevelCommand tokens\n                |> Option.defaultValue String.Empty\n\n            let isHistory = commandName.Equals(\"history\", StringComparison.InvariantCultureIgnoreCase)\n\n            if isHistory\n               && not historyConfig.RecordHistoryCommands then\n                false\n            else\n                true\n\n    let recordInvocation (input: RecordInput) =\n        let loadResult = UserConfiguration.loadUserConfiguration ()\n\n        if not\n           <| shouldRecord input loadResult.Configuration.History then\n            None\n        else\n            let redactedNormalized, redactions = redactArguments input.argvNormalized loadResult.Configuration.History\n\n            let redactedOriginal, _ = redactArguments input.argvOriginal loadResult.Configuration.History\n\n            let entry =\n                let repoRoot = tryFindRepoRoot input.cwd\n                let repoName = tryGetRepoName repoRoot\n                let repoBranch = tryGetGitBranch repoRoot\n\n                {\n                    id = Guid.NewGuid()\n                    timestampUtc = input.timestampUtc\n                    argvOriginal = redactedOriginal\n                    argvNormalized = redactedNormalized\n                    commandLine = buildCommandLine redactedNormalized\n                    cwd = input.cwd\n                    repoRoot = repoRoot\n                    repoName = repoName\n                    repoBranch = repoBranch\n                    graceVersion = getGraceVersion ()\n                    exitCode = input.exitCode\n                    durationMs = input.durationMs\n                    parseSucceeded = input.parseSucceeded\n                    redactions = redactions\n                    source = normalizeSourceOption input.source\n                }\n\n            Some(entry, loadResult.Configuration.History)\n\n    let tryRecordInvocation (input: RecordInput) =\n        match recordInvocation input with\n        | None -> ()\n        | Some (entry, historyConfig) ->\n            let onFailure () = Console.Error.WriteLine(\"Grace history: failed to acquire history lock; skipping history recording.\")\n\n            withHistoryLock\n                (fun () ->\n                    appendHistoryEntry entry historyConfig\n                    ())\n                onFailure\n\n    let clearHistory () =\n        let onFailure () = Error \"Grace history: failed to acquire history lock.\"\n\n        withHistoryLock\n            (fun () ->\n                ensureHistoryDirectory ()\n                let path = getHistoryFilePath ()\n\n                let removedCount =\n                    if File.Exists(path) then\n                        File.ReadLines(path)\n                        |> Seq.filter (fun line -> not <| String.IsNullOrWhiteSpace(line))\n                        |> Seq.length\n                    else\n                        0\n\n                File.WriteAllText(path, String.Empty)\n                Ok removedCount)\n            onFailure\n\n    let isDestructive (commandLine: string) (historyConfig: UserConfiguration.HistoryConfiguration) =\n        let patterns = historyConfig.DestructiveTokenRegexes\n        let regexes = buildRegexes patterns\n\n        regexes\n        |> List.exists (fun regex -> regex.IsMatch(commandLine))\n"
  },
  {
    "path": "src/Grace.CLI/LocalStateDb.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.Threading\nopen System.Threading.Tasks\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Microsoft.Data.Sqlite\nopen NodaTime\nopen SQLitePCL\n\nmodule LocalStateDb =\n    [<Literal>]\n    let private SchemaVersion = \"2\"\n\n    [<Literal>]\n    let private BusyTimeoutMs = 30000\n\n    let private retryDelaysMs = [| 50; 100; 200; 400; 800; 1600 |]\n\n    let mutable private verboseEnabled = false\n\n    let setVerbose enabled = verboseEnabled <- enabled\n    let private traceFilePath = Environment.GetEnvironmentVariable(\"GRACE_LOCALSTATE_DB_TRACE_PATH\")\n    let private traceOpenConnections = not (String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(\"GRACE_LOCALSTATE_DB_TRACE_OPEN\")))\n    let private initLocks = ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase)\n    let private initializedDbs = ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase)\n\n    let private sqliteInitialized =\n        lazy\n            (Batteries_V2.Init()\n             true)\n\n    let private logVerbose message = if verboseEnabled then Log.LogVerbose message\n\n    let private logTrace message =\n        if not (String.IsNullOrWhiteSpace(traceFilePath)) then\n            try\n                File.AppendAllText(traceFilePath, $\"{DateTime.UtcNow:O} {message}{Environment.NewLine}\")\n            with\n            | _ -> ()\n\n    let private logTraceStatement label (statement: string) =\n        let trimmed =\n            if statement.Length > 240 then\n                statement.Substring(0, 240) + \"...\"\n            else\n                statement\n\n        logTrace $\"{label}: {trimmed}\"\n\n    let private isBusyOrLocked (ex: SqliteException) = ex.SqliteErrorCode = 5 || ex.SqliteErrorCode = 6\n\n    let private executeWithRetry (operation: unit -> Task<unit>) =\n        let rec run attempt =\n            task {\n                try\n                    do! operation ()\n                with\n                | :? SqliteException as ex when isBusyOrLocked ex ->\n                    if attempt >= retryDelaysMs.Length then return raise ex\n                    let jitter = Random.Shared.Next(0, 50)\n                    let delayMs = retryDelaysMs[attempt] + jitter\n                    do! Task.Delay(delayMs)\n                    return! run (attempt + 1)\n                | ex -> return raise ex\n            }\n\n        run 0\n\n    let private executeNonQuery (connection: SqliteConnection) (sql: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        cmd.ExecuteNonQuery() |> ignore\n\n    let private executePragma (connection: SqliteConnection) (sql: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        cmd.ExecuteNonQuery() |> ignore\n\n    let private executeNonQueryWithParams (connection: SqliteConnection) (sql: string) (configureParameters: SqliteParameterCollection -> unit) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        configureParameters cmd.Parameters\n        cmd.ExecuteNonQuery() |> ignore\n\n    let private applyConnectionPragmas (connection: SqliteConnection) =\n        executePragma connection $\"PRAGMA busy_timeout = {BusyTimeoutMs};\"\n        executePragma connection \"PRAGMA foreign_keys = ON;\"\n        executePragma connection \"PRAGMA synchronous = NORMAL;\"\n        executePragma connection \"PRAGMA temp_store = MEMORY;\"\n\n    let private ensureJournalMode (connection: SqliteConnection) = executePragma connection \"PRAGMA journal_mode = WAL;\"\n\n    let private openConnection (dbPath: string) =\n        sqliteInitialized.Value |> ignore\n        let directoryPath = Path.GetDirectoryName(dbPath)\n        logVerbose $\"LocalStateDb.openConnection starting. dbPath={dbPath} dir={directoryPath}\"\n\n        if traceOpenConnections then\n            logTrace $\"openConnection starting. dbPath={dbPath} dir={directoryPath}\"\n\n        let stopwatch = Stopwatch.StartNew()\n        Directory.CreateDirectory(directoryPath) |> ignore\n        logVerbose $\"LocalStateDb.openConnection directory ensured in {stopwatch.ElapsedMilliseconds}ms\"\n\n        if traceOpenConnections then\n            logTrace $\"openConnection directory ensured in {stopwatch.ElapsedMilliseconds}ms\"\n\n        let connectionString =\n            let builder = SqliteConnectionStringBuilder()\n            builder.DataSource <- dbPath\n            builder.Mode <- SqliteOpenMode.ReadWriteCreate\n            builder.Pooling <- true\n            builder.DefaultTimeout <- BusyTimeoutMs / 1000\n            builder.ToString()\n\n        let connection = new SqliteConnection(connectionString)\n\n        try\n            connection.Open()\n            applyConnectionPragmas connection\n            logVerbose $\"LocalStateDb.openConnection opened connection in {stopwatch.ElapsedMilliseconds}ms\"\n\n            if traceOpenConnections then\n                logTrace $\"openConnection opened connection in {stopwatch.ElapsedMilliseconds}ms\"\n\n            connection\n        with\n        | ex ->\n            try\n                connection.Dispose()\n            with\n            | _ -> ()\n\n            raise ex\n\n    let private schemaStatements =\n        [|\n            \"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\"\n            \"CREATE TABLE IF NOT EXISTS status_meta (id INTEGER PRIMARY KEY CHECK (id = 1), root_directory_version_id TEXT NOT NULL, root_directory_sha256_hash TEXT NOT NULL, last_successful_file_upload_unix_ticks INTEGER NOT NULL, last_successful_directory_version_upload_unix_ticks INTEGER NOT NULL);\"\n            \"CREATE TABLE IF NOT EXISTS status_directories (relative_path TEXT PRIMARY KEY, parent_path TEXT NOT NULL, directory_version_id TEXT NOT NULL, sha256_hash TEXT NOT NULL, size_bytes INTEGER NOT NULL, created_at_unix_ticks INTEGER NOT NULL, last_write_time_utc_ticks INTEGER NOT NULL);\"\n            \"CREATE INDEX IF NOT EXISTS ix_status_directories_parent ON status_directories(parent_path);\"\n            \"CREATE UNIQUE INDEX IF NOT EXISTS ix_status_directories_directory_version_id ON status_directories(directory_version_id);\"\n            \"CREATE TABLE IF NOT EXISTS status_files (relative_path TEXT PRIMARY KEY, directory_path TEXT NOT NULL, directory_version_id TEXT NOT NULL, sha256_hash TEXT NOT NULL, is_binary INTEGER NOT NULL, size_bytes INTEGER NOT NULL, created_at_unix_ticks INTEGER NOT NULL, uploaded_to_object_storage INTEGER NOT NULL, last_write_time_utc_ticks INTEGER NOT NULL, FOREIGN KEY (directory_version_id) REFERENCES status_directories(directory_version_id) ON DELETE CASCADE);\"\n            \"CREATE INDEX IF NOT EXISTS ix_status_files_directory_path ON status_files(directory_path);\"\n            \"CREATE INDEX IF NOT EXISTS ix_status_files_directory_version_id ON status_files(directory_version_id);\"\n            \"CREATE INDEX IF NOT EXISTS ix_status_files_sha256 ON status_files(sha256_hash);\"\n            \"CREATE TABLE IF NOT EXISTS object_cache_directories (directory_version_id TEXT PRIMARY KEY, relative_path TEXT NOT NULL, sha256_hash TEXT NOT NULL, size_bytes INTEGER NOT NULL, created_at_unix_ticks INTEGER NOT NULL, last_write_time_utc_ticks INTEGER NOT NULL);\"\n            \"CREATE INDEX IF NOT EXISTS ix_object_cache_directories_relative_path ON object_cache_directories(relative_path);\"\n            \"CREATE TABLE IF NOT EXISTS object_cache_directory_children (parent_directory_version_id TEXT NOT NULL, child_directory_version_id TEXT NOT NULL, ordinal INTEGER NOT NULL, PRIMARY KEY (parent_directory_version_id, child_directory_version_id), FOREIGN KEY (parent_directory_version_id) REFERENCES object_cache_directories(directory_version_id) ON DELETE CASCADE, FOREIGN KEY (child_directory_version_id) REFERENCES object_cache_directories(directory_version_id) ON DELETE RESTRICT);\"\n            \"CREATE INDEX IF NOT EXISTS ix_object_cache_children_parent ON object_cache_directory_children(parent_directory_version_id);\"\n            \"CREATE TABLE IF NOT EXISTS object_cache_directory_files (directory_version_id TEXT NOT NULL, relative_path TEXT NOT NULL, sha256_hash TEXT NOT NULL, is_binary INTEGER NOT NULL, size_bytes INTEGER NOT NULL, created_at_unix_ticks INTEGER NOT NULL, uploaded_to_object_storage INTEGER NOT NULL, last_write_time_utc_ticks INTEGER NOT NULL, PRIMARY KEY (directory_version_id, relative_path), FOREIGN KEY (directory_version_id) REFERENCES object_cache_directories(directory_version_id) ON DELETE CASCADE);\"\n            \"CREATE INDEX IF NOT EXISTS ix_object_cache_files_path_hash ON object_cache_directory_files(relative_path, sha256_hash);\"\n        |]\n\n    let private tryGetMetaValue (connection: SqliteConnection) (key: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- \"SELECT value FROM meta WHERE key = $key LIMIT 1;\"\n        cmd.Parameters.AddWithValue(\"$key\", key) |> ignore\n        use reader = cmd.ExecuteReader()\n        if reader.Read() then Some(reader.GetString(0)) else None\n\n    let private setMetaValue (connection: SqliteConnection) (key: string) (value: string) =\n        executeNonQueryWithParams connection \"INSERT OR REPLACE INTO meta (key, value) VALUES ($key, $value);\" (fun parameters ->\n            parameters.AddWithValue(\"$key\", key) |> ignore\n            parameters.AddWithValue(\"$value\", value) |> ignore)\n\n    let private insertStatusMetaIfMissing (connection: SqliteConnection) =\n        let defaultStatus = GraceStatus.Default\n\n        executeNonQueryWithParams\n            connection\n            \"INSERT OR IGNORE INTO status_meta (id, root_directory_version_id, root_directory_sha256_hash, last_successful_file_upload_unix_ticks, last_successful_directory_version_upload_unix_ticks) VALUES (1, $root_id, $root_hash, $last_file, $last_dir);\"\n            (fun parameters ->\n                parameters.AddWithValue(\"$root_id\", defaultStatus.RootDirectoryId.ToString())\n                |> ignore\n\n                parameters.AddWithValue(\"$root_hash\", defaultStatus.RootDirectorySha256Hash)\n                |> ignore\n\n                parameters.AddWithValue(\"$last_file\", defaultStatus.LastSuccessfulFileUpload.ToUnixTimeTicks())\n                |> ignore\n\n                parameters.AddWithValue(\"$last_dir\", defaultStatus.LastSuccessfulDirectoryVersionUpload.ToUnixTimeTicks())\n                |> ignore)\n\n    let private recreateDatabase (dbPath: string) =\n        try\n            SqliteConnection.ClearAllPools()\n        with\n        | _ -> ()\n\n        if File.Exists(dbPath) then\n            let timestamp = DateTime.UtcNow.ToString(\"yyyyMMddHHmmss\")\n            let directoryPath = Path.GetDirectoryName(dbPath)\n            let corruptPath = Path.Combine(directoryPath, $\"grace-local.corrupt.{timestamp}.db\")\n            File.Move(dbPath, corruptPath, true)\n\n        let sidecars = [| \"-wal\"; \"-shm\"; \"-journal\" |]\n\n        sidecars\n        |> Array.iter (fun suffix ->\n            let sidecarPath = dbPath + suffix\n            if File.Exists(sidecarPath) then File.Delete(sidecarPath))\n\n    let ensureDbInitialized (dbPath: string) =\n        task {\n            let normalizedPath = Path.GetFullPath(dbPath)\n            let mutable loopCount = 0\n\n            match initializedDbs.TryGetValue(normalizedPath) with\n            | true, _ -> ()\n            | _ ->\n                let semaphore = initLocks.GetOrAdd(normalizedPath, (fun _ -> new SemaphoreSlim(1, 1)))\n\n                do! semaphore.WaitAsync()\n\n                try\n                    match initializedDbs.TryGetValue(normalizedPath) with\n                    | true, _ -> ()\n                    | _ ->\n                        do!\n                            executeWithRetry (fun () ->\n                                task {\n                                    let runSchema (connection: SqliteConnection) =\n                                        ensureJournalMode connection\n\n                                        schemaStatements\n                                        |> Array.iteri (fun index statement ->\n                                            logTraceStatement $\"schema[{index}] start\" statement\n                                            executeNonQuery connection statement\n                                            logTrace $\"schema[{index}] done\")\n\n                                    let schemaExists (connection: SqliteConnection) =\n                                        use cmd = connection.CreateCommand()\n                                        cmd.CommandText <- \"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta' LIMIT 1;\"\n                                        use reader = cmd.ExecuteReader()\n                                        reader.Read()\n\n                                    let mutable recreate = false\n\n                                    do\n                                        do\n                                            try\n                                                use schemaConnection = openConnection normalizedPath\n                                                if not (schemaExists schemaConnection) then runSchema schemaConnection\n                                            with\n                                            | :? SqliteException as ex when ex.SqliteErrorCode = 26 -> recreate <- true\n\n                                        loopCount <- loopCount + 1\n                                        logTrace $\"Local state DB schema check attempt {loopCount} for {normalizedPath}\"\n\n                                        try\n                                            use connection = openConnection normalizedPath\n\n                                            try\n                                                ensureJournalMode connection\n\n                                                match tryGetMetaValue connection \"schema_version\" with\n                                                | Some version when version = SchemaVersion -> ()\n                                                | Some _ -> recreate <- true\n                                                | None ->\n                                                    logTrace \"meta schema_version missing; writing defaults\"\n                                                    let createdAtTicks = getCurrentInstant().ToUnixTimeTicks()\n                                                    setMetaValue connection \"schema_version\" SchemaVersion\n                                                    setMetaValue connection \"created_at_unix_ticks\" $\"{createdAtTicks}\"\n\n                                                if not recreate then\n                                                    logTrace \"status_meta ensuring default row\"\n                                                    insertStatusMetaIfMissing connection\n                                            with\n                                            | :? SqliteException as ex when ex.SqliteErrorCode = 26 -> recreate <- true\n                                        with\n                                        | :? SqliteException as ex when ex.SqliteErrorCode = 26 -> recreate <- true\n\n                                    if recreate then\n                                        logVerbose $\"Local state DB schema mismatch or corruption detected. Recreating {normalizedPath}.\"\n                                        logTrace \"recreateDatabase triggered\"\n                                        recreateDatabase normalizedPath\n\n                                        do\n                                            use schemaConnection = openConnection normalizedPath\n                                            runSchema schemaConnection\n\n                                        use connection = openConnection normalizedPath\n                                        ensureJournalMode connection\n                                        setMetaValue connection \"schema_version\" SchemaVersion\n                                        setMetaValue connection \"created_at_unix_ticks\" $\"{getCurrentInstant().ToUnixTimeTicks()}\"\n                                        logTrace \"status_meta ensuring default row\"\n                                        insertStatusMetaIfMissing connection\n                                })\n\n                        initializedDbs[normalizedPath] <- true\n                finally\n                    semaphore.Release() |> ignore\n        }\n\n    type StatusMeta =\n        {\n            RootDirectoryId: DirectoryVersionId\n            RootDirectorySha256Hash: Sha256Hash\n            LastSuccessfulFileUpload: Instant\n            LastSuccessfulDirectoryVersionUpload: Instant\n        }\n\n    let private readStatusMetaInternal (connection: SqliteConnection) =\n        use cmd = connection.CreateCommand()\n\n        cmd.CommandText <-\n            \"SELECT root_directory_version_id, root_directory_sha256_hash, last_successful_file_upload_unix_ticks, last_successful_directory_version_upload_unix_ticks FROM status_meta WHERE id = 1;\"\n\n        use reader = cmd.ExecuteReader()\n\n        if reader.Read() then\n            let rootId = Guid.Parse(reader.GetString(0))\n            let rootHash = reader.GetString(1)\n            let lastFile = Instant.FromUnixTimeTicks(reader.GetInt64(2))\n            let lastDir = Instant.FromUnixTimeTicks(reader.GetInt64(3))\n\n            {\n                RootDirectoryId = rootId\n                RootDirectorySha256Hash = rootHash\n                LastSuccessfulFileUpload = lastFile\n                LastSuccessfulDirectoryVersionUpload = lastDir\n            }\n            |> Some\n        else\n            None\n\n    let readStatusMeta (dbPath: string) =\n        task {\n            do! ensureDbInitialized dbPath\n            let connection = openConnection dbPath\n\n            try\n                match readStatusMetaInternal connection with\n                | Some meta -> return meta\n                | None ->\n                    let defaultStatus = GraceStatus.Default\n\n                    return\n                        {\n                            RootDirectoryId = defaultStatus.RootDirectoryId\n                            RootDirectorySha256Hash = defaultStatus.RootDirectorySha256Hash\n                            LastSuccessfulFileUpload = defaultStatus.LastSuccessfulFileUpload\n                            LastSuccessfulDirectoryVersionUpload = defaultStatus.LastSuccessfulDirectoryVersionUpload\n                        }\n            finally\n                connection.Dispose()\n        }\n\n    let private setStatusMeta (connection: SqliteConnection) (graceStatus: GraceStatus) =\n        executeNonQueryWithParams\n            connection\n            \"INSERT OR REPLACE INTO status_meta (id, root_directory_version_id, root_directory_sha256_hash, last_successful_file_upload_unix_ticks, last_successful_directory_version_upload_unix_ticks) VALUES (1, $root_id, $root_hash, $last_file, $last_dir);\"\n            (fun parameters ->\n                parameters.AddWithValue(\"$root_id\", graceStatus.RootDirectoryId.ToString())\n                |> ignore\n\n                parameters.AddWithValue(\"$root_hash\", graceStatus.RootDirectorySha256Hash)\n                |> ignore\n\n                parameters.AddWithValue(\"$last_file\", graceStatus.LastSuccessfulFileUpload.ToUnixTimeTicks())\n                |> ignore\n\n                parameters.AddWithValue(\"$last_dir\", graceStatus.LastSuccessfulDirectoryVersionUpload.ToUnixTimeTicks())\n                |> ignore)\n\n    let replaceStatusSnapshot (dbPath: string) (graceStatus: GraceStatus) =\n        task {\n            do! ensureDbInitialized dbPath\n\n            return!\n                executeWithRetry (fun () ->\n                    task {\n                        let connection = openConnection dbPath\n\n                        try\n                            executeNonQuery connection \"BEGIN IMMEDIATE;\"\n\n                            try\n                                executeNonQuery connection \"DELETE FROM status_directories;\"\n                                executeNonQuery connection \"DELETE FROM status_files;\"\n                                setStatusMeta connection graceStatus\n\n                                use directoryCommand = connection.CreateCommand()\n\n                                directoryCommand.CommandText <-\n                                    \"INSERT OR REPLACE INTO status_directories (relative_path, parent_path, directory_version_id, sha256_hash, size_bytes, created_at_unix_ticks, last_write_time_utc_ticks) VALUES ($relative_path, $parent_path, $directory_version_id, $sha256_hash, $size_bytes, $created_at, $last_write);\"\n\n                                directoryCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$parent_path\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$sha256_hash\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$size_bytes\", SqliteType.Integer)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$created_at\", SqliteType.Integer)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$last_write\", SqliteType.Integer)\n                                |> ignore\n\n                                use fileCommand = connection.CreateCommand()\n\n                                fileCommand.CommandText <-\n                                    \"INSERT OR REPLACE INTO status_files (relative_path, directory_path, directory_version_id, sha256_hash, is_binary, size_bytes, created_at_unix_ticks, uploaded_to_object_storage, last_write_time_utc_ticks) VALUES ($relative_path, $directory_path, $directory_version_id, $sha256_hash, $is_binary, $size_bytes, $created_at, $uploaded, $last_write);\"\n\n                                fileCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$directory_path\", SqliteType.Text)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$sha256_hash\", SqliteType.Text)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$is_binary\", SqliteType.Integer)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$size_bytes\", SqliteType.Integer)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$created_at\", SqliteType.Integer)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$uploaded\", SqliteType.Integer)\n                                |> ignore\n\n                                fileCommand.Parameters.Add(\"$last_write\", SqliteType.Integer)\n                                |> ignore\n\n                                graceStatus.Index.Values\n                                |> Seq.iter (fun directory ->\n                                    let parentPath =\n                                        match getParentPath directory.RelativePath with\n                                        | Some path -> path\n                                        | None -> String.Empty\n\n                                    directoryCommand.Parameters[\"$relative_path\"].Value <- directory.RelativePath\n                                    directoryCommand.Parameters[\"$parent_path\"].Value <- parentPath\n                                    directoryCommand.Parameters[\"$directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                    directoryCommand.Parameters[\"$sha256_hash\"].Value <- directory.Sha256Hash\n                                    directoryCommand.Parameters[\"$size_bytes\"].Value <- directory.Size\n                                    directoryCommand.Parameters[\"$created_at\"].Value <- directory.CreatedAt.ToUnixTimeTicks()\n                                    directoryCommand.Parameters[\"$last_write\"].Value <- directory.LastWriteTimeUtc.Ticks\n                                    directoryCommand.ExecuteNonQuery() |> ignore\n\n                                    directory.Files\n                                    |> Seq.iter (fun file ->\n                                        fileCommand.Parameters[\"$relative_path\"].Value <- file.RelativePath\n                                        fileCommand.Parameters[\"$directory_path\"].Value <- directory.RelativePath\n                                        fileCommand.Parameters[\"$directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                        fileCommand.Parameters[\"$sha256_hash\"].Value <- file.Sha256Hash\n                                        fileCommand.Parameters[\"$is_binary\"].Value <- if file.IsBinary then 1 else 0\n                                        fileCommand.Parameters[\"$size_bytes\"].Value <- file.Size\n                                        fileCommand.Parameters[\"$created_at\"].Value <- file.CreatedAt.ToUnixTimeTicks()\n                                        fileCommand.Parameters[\"$uploaded\"].Value <- if file.UploadedToObjectStorage then 1 else 0\n                                        fileCommand.Parameters[\"$last_write\"].Value <- file.LastWriteTimeUtc.Ticks\n                                        fileCommand.ExecuteNonQuery() |> ignore))\n\n                                executeNonQuery connection \"COMMIT;\"\n                            with\n                            | ex ->\n                                executeNonQuery connection \"ROLLBACK;\"\n                                return raise ex\n                        finally\n                            connection.Dispose()\n                    })\n        }\n\n    let upsertObjectCache (dbPath: string) (newDirectoryVersions: IEnumerable<LocalDirectoryVersion>) =\n        task {\n            do! ensureDbInitialized dbPath\n            let directoriesToUpsert = newDirectoryVersions |> Seq.toArray\n\n            return!\n                executeWithRetry (fun () ->\n                    task {\n                        let connection = openConnection dbPath\n\n                        try\n                            executeNonQuery connection \"BEGIN IMMEDIATE;\"\n\n                            try\n                                let knownDirectoryIds = HashSet<string>(StringComparer.OrdinalIgnoreCase)\n\n                                use knownDirectoryIdsCommand = connection.CreateCommand()\n                                knownDirectoryIdsCommand.CommandText <- \"SELECT directory_version_id FROM object_cache_directories;\"\n\n                                use knownDirectoryIdsReader = knownDirectoryIdsCommand.ExecuteReader()\n\n                                while knownDirectoryIdsReader.Read() do\n                                    knownDirectoryIds.Add(knownDirectoryIdsReader.GetString(0))\n                                    |> ignore\n\n                                use directoryCommand = connection.CreateCommand()\n\n                                directoryCommand.CommandText <-\n                                    \"INSERT INTO object_cache_directories (directory_version_id, relative_path, sha256_hash, size_bytes, created_at_unix_ticks, last_write_time_utc_ticks) VALUES ($directory_version_id, $relative_path, $sha256_hash, $size_bytes, $created_at, $last_write) ON CONFLICT(directory_version_id) DO UPDATE SET relative_path = excluded.relative_path, sha256_hash = excluded.sha256_hash, size_bytes = excluded.size_bytes, created_at_unix_ticks = excluded.created_at_unix_ticks, last_write_time_utc_ticks = excluded.last_write_time_utc_ticks;\"\n\n                                directoryCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$sha256_hash\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$size_bytes\", SqliteType.Integer)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$created_at\", SqliteType.Integer)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$last_write\", SqliteType.Integer)\n                                |> ignore\n\n                                use deleteChildrenCommand = connection.CreateCommand()\n\n                                deleteChildrenCommand.CommandText <-\n                                    \"DELETE FROM object_cache_directory_children WHERE parent_directory_version_id = $parent_directory_version_id;\"\n\n                                deleteChildrenCommand.Parameters.Add(\"$parent_directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                use insertChildCommand = connection.CreateCommand()\n\n                                insertChildCommand.CommandText <-\n                                    \"INSERT INTO object_cache_directory_children (parent_directory_version_id, child_directory_version_id, ordinal) VALUES ($parent_directory_version_id, $child_directory_version_id, $ordinal) ON CONFLICT(parent_directory_version_id, child_directory_version_id) DO UPDATE SET ordinal = excluded.ordinal;\"\n\n                                insertChildCommand.Parameters.Add(\"$parent_directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                insertChildCommand.Parameters.Add(\"$child_directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                insertChildCommand.Parameters.Add(\"$ordinal\", SqliteType.Integer)\n                                |> ignore\n\n                                use deleteFilesCommand = connection.CreateCommand()\n                                deleteFilesCommand.CommandText <- \"DELETE FROM object_cache_directory_files WHERE directory_version_id = $directory_version_id;\"\n\n                                deleteFilesCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                use insertFileCommand = connection.CreateCommand()\n\n                                insertFileCommand.CommandText <-\n                                    \"INSERT INTO object_cache_directory_files (directory_version_id, relative_path, sha256_hash, is_binary, size_bytes, created_at_unix_ticks, uploaded_to_object_storage, last_write_time_utc_ticks) VALUES ($directory_version_id, $relative_path, $sha256_hash, $is_binary, $size_bytes, $created_at, $uploaded, $last_write) ON CONFLICT(directory_version_id, relative_path) DO UPDATE SET sha256_hash = excluded.sha256_hash, is_binary = excluded.is_binary, size_bytes = excluded.size_bytes, created_at_unix_ticks = excluded.created_at_unix_ticks, uploaded_to_object_storage = excluded.uploaded_to_object_storage, last_write_time_utc_ticks = excluded.last_write_time_utc_ticks;\"\n\n                                insertFileCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$sha256_hash\", SqliteType.Text)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$is_binary\", SqliteType.Integer)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$size_bytes\", SqliteType.Integer)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$created_at\", SqliteType.Integer)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$uploaded\", SqliteType.Integer)\n                                |> ignore\n\n                                insertFileCommand.Parameters.Add(\"$last_write\", SqliteType.Integer)\n                                |> ignore\n\n                                // Pass 1: Ensure all directory rows exist before adding any FK-dependent rows.\n                                directoriesToUpsert\n                                |> Seq.iter (fun directory ->\n                                    let directoryVersionId = directory.DirectoryVersionId.ToString()\n                                    directoryCommand.Parameters[\"$directory_version_id\"].Value <- directoryVersionId\n                                    directoryCommand.Parameters[\"$relative_path\"].Value <- directory.RelativePath\n                                    directoryCommand.Parameters[\"$sha256_hash\"].Value <- directory.Sha256Hash\n                                    directoryCommand.Parameters[\"$size_bytes\"].Value <- directory.Size\n                                    directoryCommand.Parameters[\"$created_at\"].Value <- directory.CreatedAt.ToUnixTimeTicks()\n                                    directoryCommand.Parameters[\"$last_write\"].Value <- directory.LastWriteTimeUtc.Ticks\n                                    directoryCommand.ExecuteNonQuery() |> ignore\n                                    knownDirectoryIds.Add(directoryVersionId) |> ignore)\n\n                                // Pass 2: Refresh child and file links for each upserted directory.\n                                directoriesToUpsert\n                                |> Seq.iter (fun directory ->\n                                    deleteChildrenCommand.Parameters[\"$parent_directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                    deleteChildrenCommand.ExecuteNonQuery() |> ignore\n\n                                    deleteFilesCommand.Parameters[\"$directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                    deleteFilesCommand.ExecuteNonQuery() |> ignore\n\n                                    directory.Directories\n                                    |> Seq.iteri (fun index childId ->\n                                        let childDirectoryVersionId = childId.ToString()\n\n                                        if knownDirectoryIds.Contains(childDirectoryVersionId) then\n                                            insertChildCommand.Parameters[\"$parent_directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                            insertChildCommand.Parameters[\"$child_directory_version_id\"].Value <- childDirectoryVersionId\n                                            insertChildCommand.Parameters[\"$ordinal\"].Value <- index\n                                            insertChildCommand.ExecuteNonQuery() |> ignore\n                                        else\n                                            invalidOp\n                                                $\"Cannot upsert object cache because child DirectoryVersionId {childDirectoryVersionId} is missing. Parent DirectoryVersionId: {directory.DirectoryVersionId}.\"\n                                    )\n\n                                    directory.Files\n                                    |> Seq.iter (fun file ->\n                                        insertFileCommand.Parameters[\"$directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                        insertFileCommand.Parameters[\"$relative_path\"].Value <- file.RelativePath\n                                        insertFileCommand.Parameters[\"$sha256_hash\"].Value <- file.Sha256Hash\n                                        insertFileCommand.Parameters[\"$is_binary\"].Value <- if file.IsBinary then 1 else 0\n                                        insertFileCommand.Parameters[\"$size_bytes\"].Value <- file.Size\n                                        insertFileCommand.Parameters[\"$created_at\"].Value <- file.CreatedAt.ToUnixTimeTicks()\n                                        insertFileCommand.Parameters[\"$uploaded\"].Value <- if file.UploadedToObjectStorage then 1 else 0\n                                        insertFileCommand.Parameters[\"$last_write\"].Value <- file.LastWriteTimeUtc.Ticks\n                                        insertFileCommand.ExecuteNonQuery() |> ignore))\n\n                                executeNonQuery connection \"COMMIT;\"\n                            with\n                            | ex ->\n                                executeNonQuery connection \"ROLLBACK;\"\n                                return raise ex\n                        finally\n                            connection.Dispose()\n                    })\n        }\n\n    let isFileVersionInObjectCache (dbPath: string) (fileVersion: LocalFileVersion) =\n        task {\n            do! ensureDbInitialized dbPath\n            let connection = openConnection dbPath\n\n            try\n                use cmd = connection.CreateCommand()\n                cmd.CommandText <- \"SELECT 1 FROM object_cache_directory_files WHERE relative_path = $relative_path AND sha256_hash = $sha256_hash LIMIT 1;\"\n\n                cmd.Parameters.AddWithValue(\"$relative_path\", fileVersion.RelativePath)\n                |> ignore\n\n                cmd.Parameters.AddWithValue(\"$sha256_hash\", fileVersion.Sha256Hash)\n                |> ignore\n\n                use reader = cmd.ExecuteReader()\n                return reader.Read()\n            finally\n                connection.Dispose()\n        }\n\n    let isDirectoryVersionInObjectCache (dbPath: string) (directoryVersionId: DirectoryVersionId) =\n        task {\n            do! ensureDbInitialized dbPath\n            let connection = openConnection dbPath\n\n            try\n                use cmd = connection.CreateCommand()\n                cmd.CommandText <- \"SELECT 1 FROM object_cache_directories WHERE directory_version_id = $id LIMIT 1;\"\n\n                cmd.Parameters.AddWithValue(\"$id\", directoryVersionId.ToString())\n                |> ignore\n\n                use reader = cmd.ExecuteReader()\n                return reader.Read()\n            finally\n                connection.Dispose()\n        }\n\n    let removeObjectCacheDirectory (dbPath: string) (directoryVersionId: DirectoryVersionId) =\n        task {\n            do! ensureDbInitialized dbPath\n\n            return!\n                executeWithRetry (fun () ->\n                    task {\n                        let connection = openConnection dbPath\n\n                        try\n                            executeNonQuery connection \"BEGIN IMMEDIATE;\"\n\n                            try\n                                use cmd = connection.CreateCommand()\n                                cmd.CommandText <- \"DELETE FROM object_cache_directories WHERE directory_version_id = $id;\"\n\n                                cmd.Parameters.AddWithValue(\"$id\", directoryVersionId.ToString())\n                                |> ignore\n\n                                cmd.ExecuteNonQuery() |> ignore\n                                executeNonQuery connection \"COMMIT;\"\n                            with\n                            | ex ->\n                                executeNonQuery connection \"ROLLBACK;\"\n                                return raise ex\n                        finally\n                            connection.Dispose()\n                    })\n        }\n\n    let applyStatusIncremental\n        (dbPath: string)\n        (newGraceStatus: GraceStatus)\n        (newDirectoryVersions: IEnumerable<LocalDirectoryVersion>)\n        (differences: IEnumerable<FileSystemDifference>)\n        =\n        task {\n            do! ensureDbInitialized dbPath\n\n            return!\n                executeWithRetry (fun () ->\n                    task {\n                        let connection = openConnection dbPath\n\n                        try\n                            executeNonQuery connection \"BEGIN IMMEDIATE;\"\n\n                            try\n                                setStatusMeta connection newGraceStatus\n\n                                use directoryCommand = connection.CreateCommand()\n\n                                directoryCommand.CommandText <-\n                                    \"INSERT OR REPLACE INTO status_directories (relative_path, parent_path, directory_version_id, sha256_hash, size_bytes, created_at_unix_ticks, last_write_time_utc_ticks) VALUES ($relative_path, $parent_path, $directory_version_id, $sha256_hash, $size_bytes, $created_at, $last_write);\"\n\n                                directoryCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$parent_path\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$sha256_hash\", SqliteType.Text)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$size_bytes\", SqliteType.Integer)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$created_at\", SqliteType.Integer)\n                                |> ignore\n\n                                directoryCommand.Parameters.Add(\"$last_write\", SqliteType.Integer)\n                                |> ignore\n\n                                newDirectoryVersions\n                                |> Seq.iter (fun directory ->\n                                    let parentPath =\n                                        match getParentPath directory.RelativePath with\n                                        | Some path -> path\n                                        | None -> String.Empty\n\n                                    directoryCommand.Parameters[\"$relative_path\"].Value <- directory.RelativePath\n                                    directoryCommand.Parameters[\"$parent_path\"].Value <- parentPath\n                                    directoryCommand.Parameters[\"$directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                    directoryCommand.Parameters[\"$sha256_hash\"].Value <- directory.Sha256Hash\n                                    directoryCommand.Parameters[\"$size_bytes\"].Value <- directory.Size\n                                    directoryCommand.Parameters[\"$created_at\"].Value <- directory.CreatedAt.ToUnixTimeTicks()\n                                    directoryCommand.Parameters[\"$last_write\"].Value <- directory.LastWriteTimeUtc.Ticks\n                                    directoryCommand.ExecuteNonQuery() |> ignore)\n\n                                use fileUpsertCommand = connection.CreateCommand()\n\n                                fileUpsertCommand.CommandText <-\n                                    \"INSERT OR REPLACE INTO status_files (relative_path, directory_path, directory_version_id, sha256_hash, is_binary, size_bytes, created_at_unix_ticks, uploaded_to_object_storage, last_write_time_utc_ticks) VALUES ($relative_path, $directory_path, $directory_version_id, $sha256_hash, $is_binary, $size_bytes, $created_at, $uploaded, $last_write);\"\n\n                                fileUpsertCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$directory_path\", SqliteType.Text)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$directory_version_id\", SqliteType.Text)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$sha256_hash\", SqliteType.Text)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$is_binary\", SqliteType.Integer)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$size_bytes\", SqliteType.Integer)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$created_at\", SqliteType.Integer)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$uploaded\", SqliteType.Integer)\n                                |> ignore\n\n                                fileUpsertCommand.Parameters.Add(\"$last_write\", SqliteType.Integer)\n                                |> ignore\n\n                                use fileDeleteCommand = connection.CreateCommand()\n                                fileDeleteCommand.CommandText <- \"DELETE FROM status_files WHERE relative_path = $relative_path;\"\n\n                                fileDeleteCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                use directoryDeleteCommand = connection.CreateCommand()\n                                directoryDeleteCommand.CommandText <- \"DELETE FROM status_directories WHERE relative_path = $relative_path;\"\n\n                                directoryDeleteCommand.Parameters.Add(\"$relative_path\", SqliteType.Text)\n                                |> ignore\n\n                                // Upsert every file in each changed/new directory version. This keeps unchanged sibling files\n                                // attached to the new directory_version_id when a directory row is replaced.\n                                newDirectoryVersions\n                                |> Seq.collect (fun directory -> directory.Files |> Seq.map (fun file -> (file, directory)))\n                                |> Seq.iter (fun (file, directory) ->\n                                    fileUpsertCommand.Parameters[\"$relative_path\"].Value <- file.RelativePath\n                                    fileUpsertCommand.Parameters[\"$directory_path\"].Value <- directory.RelativePath\n                                    fileUpsertCommand.Parameters[\"$directory_version_id\"].Value <- directory.DirectoryVersionId.ToString()\n                                    fileUpsertCommand.Parameters[\"$sha256_hash\"].Value <- file.Sha256Hash\n                                    fileUpsertCommand.Parameters[\"$is_binary\"].Value <- if file.IsBinary then 1 else 0\n                                    fileUpsertCommand.Parameters[\"$size_bytes\"].Value <- file.Size\n                                    fileUpsertCommand.Parameters[\"$created_at\"].Value <- file.CreatedAt.ToUnixTimeTicks()\n                                    fileUpsertCommand.Parameters[\"$uploaded\"].Value <- if file.UploadedToObjectStorage then 1 else 0\n                                    fileUpsertCommand.Parameters[\"$last_write\"].Value <- file.LastWriteTimeUtc.Ticks\n                                    fileUpsertCommand.ExecuteNonQuery() |> ignore)\n\n                                differences\n                                |> Seq.iter (fun difference ->\n                                    if difference.DifferenceType = Delete then\n                                        if difference.FileSystemEntryType.IsFile then\n                                            fileDeleteCommand.Parameters[\"$relative_path\"].Value <- difference.RelativePath\n                                            fileDeleteCommand.ExecuteNonQuery() |> ignore\n                                        else\n                                            directoryDeleteCommand.Parameters[\"$relative_path\"].Value <- difference.RelativePath\n                                            directoryDeleteCommand.ExecuteNonQuery() |> ignore)\n\n                                executeNonQuery connection \"COMMIT;\"\n                            with\n                            | ex ->\n                                executeNonQuery connection \"ROLLBACK;\"\n                                return raise ex\n                        finally\n                            connection.Dispose()\n                    })\n        }\n\n    type private StatusDirectoryRow =\n        {\n            RelativePath: string\n            ParentPath: string\n            DirectoryVersionId: DirectoryVersionId\n            Sha256Hash: Sha256Hash\n            SizeBytes: int64\n            CreatedAt: Instant\n            LastWriteTimeUtc: DateTime\n        }\n\n    type private StatusFileRow =\n        {\n            RelativePath: string\n            DirectoryVersionId: DirectoryVersionId\n            Sha256Hash: Sha256Hash\n            IsBinary: bool\n            SizeBytes: int64\n            CreatedAt: Instant\n            UploadedToObjectStorage: bool\n            LastWriteTimeUtc: DateTime\n        }\n\n    let readStatusSnapshot (dbPath: string) =\n        task {\n            do! ensureDbInitialized dbPath\n            let connection = openConnection dbPath\n\n            try\n                let meta: StatusMeta =\n                    match readStatusMetaInternal connection with\n                    | Some value -> value\n                    | None ->\n                        let defaultStatus = GraceStatus.Default\n\n                        {\n                            RootDirectoryId = defaultStatus.RootDirectoryId\n                            RootDirectorySha256Hash = defaultStatus.RootDirectorySha256Hash\n                            LastSuccessfulFileUpload = defaultStatus.LastSuccessfulFileUpload\n                            LastSuccessfulDirectoryVersionUpload = defaultStatus.LastSuccessfulDirectoryVersionUpload\n                        }\n\n                let directories = List<StatusDirectoryRow>()\n                let files = List<StatusFileRow>()\n\n                use directoryCommand = connection.CreateCommand()\n\n                directoryCommand.CommandText <-\n                    \"SELECT relative_path, parent_path, directory_version_id, sha256_hash, size_bytes, created_at_unix_ticks, last_write_time_utc_ticks FROM status_directories;\"\n\n                use directoryReader = directoryCommand.ExecuteReader()\n\n                while directoryReader.Read() do\n                    let relativePath = directoryReader.GetString(0)\n                    let parentPath = directoryReader.GetString(1)\n                    let directoryVersionId = Guid.Parse(directoryReader.GetString(2))\n                    let sha256Hash = directoryReader.GetString(3)\n                    let sizeBytes = directoryReader.GetInt64(4)\n                    let createdAt = Instant.FromUnixTimeTicks(directoryReader.GetInt64(5))\n                    let lastWriteTimeUtc = DateTime(directoryReader.GetInt64(6), DateTimeKind.Utc)\n\n                    directories.Add(\n                        {\n                            RelativePath = relativePath\n                            ParentPath = parentPath\n                            DirectoryVersionId = directoryVersionId\n                            Sha256Hash = sha256Hash\n                            SizeBytes = sizeBytes\n                            CreatedAt = createdAt\n                            LastWriteTimeUtc = lastWriteTimeUtc\n                        }\n                    )\n\n                use fileCommand = connection.CreateCommand()\n\n                fileCommand.CommandText <-\n                    \"SELECT relative_path, directory_version_id, sha256_hash, is_binary, size_bytes, created_at_unix_ticks, uploaded_to_object_storage, last_write_time_utc_ticks FROM status_files;\"\n\n                use fileReader = fileCommand.ExecuteReader()\n\n                while fileReader.Read() do\n                    let relativePath = fileReader.GetString(0)\n                    let directoryVersionId = Guid.Parse(fileReader.GetString(1))\n                    let sha256Hash = fileReader.GetString(2)\n                    let isBinary = fileReader.GetInt64(3) = 1L\n                    let sizeBytes = fileReader.GetInt64(4)\n                    let createdAt = Instant.FromUnixTimeTicks(fileReader.GetInt64(5))\n                    let uploaded = fileReader.GetInt64(6) = 1L\n                    let lastWriteTimeUtc = DateTime(fileReader.GetInt64(7), DateTimeKind.Utc)\n\n                    files.Add(\n                        {\n                            RelativePath = relativePath\n                            DirectoryVersionId = directoryVersionId\n                            Sha256Hash = sha256Hash\n                            IsBinary = isBinary\n                            SizeBytes = sizeBytes\n                            CreatedAt = createdAt\n                            UploadedToObjectStorage = uploaded\n                            LastWriteTimeUtc = lastWriteTimeUtc\n                        }\n                    )\n\n                let directoriesByParent = Dictionary<string, List<DirectoryVersionId>>()\n                let filesByDirectory = Dictionary<DirectoryVersionId, List<LocalFileVersion>>()\n\n                directories\n                |> Seq.iter (fun directory ->\n                    let parentPath = directory.ParentPath\n                    let mutable existing = Unchecked.defaultof<List<DirectoryVersionId>>\n\n                    if directoriesByParent.TryGetValue(parentPath, &existing) then\n                        existing.Add(directory.DirectoryVersionId)\n                    else\n                        directoriesByParent.Add(parentPath, List<DirectoryVersionId>([ directory.DirectoryVersionId ])))\n\n                files\n                |> Seq.iter (fun file ->\n                    let localFile =\n                        LocalFileVersion.Create\n                            file.RelativePath\n                            file.Sha256Hash\n                            file.IsBinary\n                            file.SizeBytes\n                            file.CreatedAt\n                            file.UploadedToObjectStorage\n                            file.LastWriteTimeUtc\n\n                    let mutable existing = Unchecked.defaultof<List<LocalFileVersion>>\n\n                    if filesByDirectory.TryGetValue(file.DirectoryVersionId, &existing) then\n                        existing.Add(localFile)\n                    else\n                        filesByDirectory.Add(file.DirectoryVersionId, List<LocalFileVersion>([ localFile ])))\n\n                let index = GraceIndex()\n\n                directories\n                |> Seq.iter (fun directory ->\n                    let directoriesForPath =\n                        let mutable list = Unchecked.defaultof<List<DirectoryVersionId>>\n\n                        if directoriesByParent.TryGetValue(directory.RelativePath, &list) then\n                            list\n                        else\n                            List<DirectoryVersionId>()\n\n                    let filesForPath =\n                        let mutable list = Unchecked.defaultof<List<LocalFileVersion>>\n\n                        if filesByDirectory.TryGetValue(directory.DirectoryVersionId, &list) then\n                            list\n                        else\n                            List<LocalFileVersion>()\n\n                    let localDirectory =\n                        LocalDirectoryVersion.Create\n                            directory.DirectoryVersionId\n                            (Current().OwnerId)\n                            (Current().OrganizationId)\n                            (Current().RepositoryId)\n                            directory.RelativePath\n                            directory.Sha256Hash\n                            directoriesForPath\n                            filesForPath\n                            directory.SizeBytes\n                            directory.LastWriteTimeUtc\n\n                    index.TryAdd(directory.DirectoryVersionId, localDirectory)\n                    |> ignore)\n\n                return\n                    {\n                        Index = index\n                        RootDirectoryId = meta.RootDirectoryId\n                        RootDirectorySha256Hash = meta.RootDirectorySha256Hash\n                        LastSuccessfulFileUpload = meta.LastSuccessfulFileUpload\n                        LastSuccessfulDirectoryVersionUpload = meta.LastSuccessfulDirectoryVersionUpload\n                    }\n            finally\n                connection.Dispose()\n        }\n"
  },
  {
    "path": "src/Grace.CLI/Log.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen System\nopen System.Globalization\nopen System.Collections.Concurrent\n\nmodule Log =\n\n    type LogLevel =\n        | Verbose\n        | Informational\n        | Error\n\n    let Log (level: LogLevel) (message: string) =\n        let pattern = \"uuuu'-'MM'-'dd'T'HH':'mm':'ss.fff\"\n        printfn $\"({getCurrentInstantExtended ()} {Utilities.getDiscriminatedUnionFullName level} {message}\"\n        ()\n\n    let LogInformational (message: string) = Log LogLevel.Informational message\n    let LogError (message: string) = Log LogLevel.Error message\n    let LogVerbose (message: string) = Log LogLevel.Verbose message\n"
  },
  {
    "path": "src/Grace.CLI/Program.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen Grace.CLI.Command\nopen Grace.CLI.Common\nopen Grace.CLI.Services\nopen Grace.CLI.Text\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Converters\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Resources.Utilities\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen NodaTime.Text\nopen Spectre.Console\nopen System\nopen System.Collections\nopen System.Collections.Generic\nopen System.CommandLine\nopen System.CommandLine.Help\nopen System.CommandLine.Parsing\nopen System.Diagnostics\nopen System.Globalization\nopen System.IO\nopen System.Linq\nopen System.Text.RegularExpressions\nopen System.Threading.Tasks\nopen Microsoft.Extensions.Caching.Memory\nopen System.CommandLine.Help\nopen FSharpPlus.Control\nopen System.CommandLine.Invocation\n\nmodule Configuration =\n\n    type GraceCLIConfiguration = { GraceWatchStatus: GraceWatchStatus }\n\n    let mutable private cliConfiguration = { GraceWatchStatus = GraceWatchStatus.Default }\n\n    let CLIConfiguration () = cliConfiguration\n    let updateConfiguration (config: GraceCLIConfiguration) = cliConfiguration <- config\n\nmodule GraceCommand =\n\n    type OptionToUpdate = { optionAlias: string; display: string; displayOnCreate: string; createParentCommand: string }\n\n    /// Built-in aliases for Grace commands.\n    let private aliases =\n        let aliases = Dictionary<string, string seq>()\n        aliases.Add(\"aliases\", [ \"alias\"; \"list\" ])\n        aliases.Add(\"branches\", [ \"repository\"; \"get-branches\" ])\n        aliases.Add(\"checkpoint\", [ \"branch\"; \"checkpoint\" ])\n        aliases.Add(\"checkpoints\", [ \"branch\"; \"get-checkpoints\" ])\n        aliases.Add(\"commit\", [ \"branch\"; \"commit\" ])\n        aliases.Add(\"commits\", [ \"branch\"; \"get-commits\" ])\n        aliases.Add(\"dir\", [ \"maint\"; \"list-contents\" ])\n        aliases.Add(\"ls\", [ \"maint\"; \"list-contents\" ])\n        aliases.Add(\"promote\", [ \"branch\"; \"promote\" ])\n        aliases.Add(\"promotions\", [ \"branch\"; \"get-promotions\" ])\n        aliases.Add(\"rebase\", [ \"branch\"; \"rebase\" ])\n        aliases.Add(\"refs\", [ \"branch\"; \"get-references\" ])\n        aliases.Add(\"save\", [ \"branch\"; \"save\" ])\n        aliases.Add(\"saves\", [ \"branch\"; \"get-saves\" ])\n        aliases.Add(\"sdir\", [ \"branch\"; \"list-contents\" ])\n        aliases.Add(\"sls\", [ \"branch\"; \"list-contents\" ])\n        aliases.Add(\"status\", [ \"branch\"; \"status\" ])\n        aliases.Add(\"switch\", [ \"branch\"; \"switch\" ])\n        aliases.Add(\"tag\", [ \"branch\"; \"tag\" ])\n        aliases.Add(\"tags\", [ \"branch\"; \"get-tags\" ])\n        //aliases.Add(\"\", [\"\"; \"\"])\n        aliases\n\n    /// The character sequences that Grace will recognize as a request for help.\n    let helpOptions = [| \"-h\"; \"/h\"; \"--help\"; \"-?\"; \"/?\" |]\n\n    /// Prints the aliases for Grace commands.\n    let printAliases () =\n        let table = Table(Border = TableBorder.DoubleEdge)\n\n        table\n            .LeftAligned()\n            .AddColumns(\n                [|\n                    TableColumn($\"[{Colors.Important}]Alias[/]\")\n                    TableColumn($\"[{Colors.Important}]Grace command[/]\")\n                |]\n            )\n        |> ignore\n\n        aliases\n        |> Seq.iter (fun alias ->\n            table.AddRow($\"grace {alias.Key}\", $\"grace {alias.Value.First()} {alias.Value.Last()}\")\n            |> ignore)\n\n        AnsiConsole.Write(table)\n\n    let internal tryGetTopLevelCommandFromArgs (args: string array) (isCaseInsensitive: bool) =\n        if isNull args || args.Length = 0 then\n            None\n        else\n            let comparison =\n                if isCaseInsensitive then\n                    StringComparison.InvariantCultureIgnoreCase\n                else\n                    StringComparison.InvariantCulture\n\n            let isOptionWithValue (token: string) =\n                token.Equals(OptionName.Output, comparison)\n                || token.Equals(\"-o\", comparison)\n                || token.Equals(OptionName.CorrelationId, comparison)\n                || token.Equals(\"-c\", comparison)\n                || token.Equals(OptionName.Source, comparison)\n\n            let rec loop index =\n                if index >= args.Length then\n                    None\n                else\n                    let token = args[index]\n\n                    if token = \"--\" then\n                        if index + 1 < args.Length then Some args[index + 1] else None\n                    elif token.StartsWith(\"-\", StringComparison.Ordinal) then\n                        let nextIndex = if isOptionWithValue token then index + 2 else index + 1\n                        loop nextIndex\n                    else\n                        Some token\n\n            loop 0\n\n    /// Gathers the available options for the current command and all its parents, which are applied hierarchically.\n    [<TailCall>]\n    let rec gatherAllOptions (command: Command) (allOptions: List<Option>) =\n        allOptions.AddRange(command.Options)\n        let parentCommand = command.Parents.OfType<Command>().FirstOrDefault()\n\n        if not <| isNull parentCommand then\n            gatherAllOptions parentCommand allOptions\n        else\n            allOptions\n\n    let private replaceDefaultValue (line: string) (defaultValueText: string) =\n        let startIndex = line.IndexOf(\"[default:\", StringComparison.OrdinalIgnoreCase)\n\n        if startIndex >= 0 then\n            let endIndex = line.IndexOf(\"]\", startIndex)\n\n            if endIndex > startIndex then\n                $\"{line.Substring(0, startIndex)}[default: {defaultValueText}]{line.Substring(endIndex + 1)}\"\n            else\n                line\n        else\n            line\n\n    let private rewriteHelpDefaults (helpText: string) (defaultsByAlias: IDictionary<string, string>) =\n        let lines = helpText.Split(Environment.NewLine)\n        let output = ResizeArray<string>(lines.Length)\n        let mutable pendingAlias: string option = None\n        let mutable i = 0\n\n        while i < lines.Length do\n            let line = lines[i]\n\n            let matchedAlias =\n                defaultsByAlias.Keys\n                |> Seq.tryFind (fun alias -> line.Contains(alias))\n\n            match matchedAlias with\n            | Some alias -> pendingAlias <- Some alias\n            | None -> ()\n\n            match pendingAlias with\n            | Some alias when line.Contains(\"[default:\", StringComparison.OrdinalIgnoreCase) ->\n                let startIndex = line.IndexOf(\"[default:\", StringComparison.OrdinalIgnoreCase)\n                let endIndex = line.IndexOf(\"]\", startIndex)\n\n                if endIndex > startIndex then\n                    output.Add(replaceDefaultValue line defaultsByAlias[alias])\n                    pendingAlias <- None\n                    i <- i + 1\n                else\n                    let prefix = line.Substring(0, startIndex)\n                    output.Add($\"{prefix}[default: {defaultsByAlias[alias]}]\")\n                    pendingAlias <- None\n\n                    let mutable j = i + 1\n                    let mutable foundEnd = false\n\n                    while j < lines.Length && not foundEnd do\n                        let continuation = lines[j]\n                        let continuationEndIndex = continuation.IndexOf(\"]\")\n\n                        if continuationEndIndex >= 0 then\n                            let suffix =\n                                if continuationEndIndex < continuation.Length - 1 then\n                                    continuation.Substring(continuationEndIndex + 1)\n                                else\n                                    String.Empty\n\n                            if not <| String.IsNullOrWhiteSpace(suffix) then output.Add(suffix)\n\n                            foundEnd <- true\n\n                        j <- j + 1\n\n                    i <- j\n            | Some alias when\n                alias = OptionName.CorrelationId\n                && line.Contains(\"CorrelationId\")\n                ->\n                if\n                    not\n                    <| line.Contains(\"[default:\", StringComparison.OrdinalIgnoreCase)\n                then\n                    output.Add($\"{line} [default: {defaultsByAlias[alias]}]\")\n                else\n                    output.Add(line)\n\n                pendingAlias <- None\n                i <- i + 1\n            | _ ->\n                output.Add(line)\n                i <- i + 1\n\n        String.Join(Environment.NewLine, output)\n\n    type HelpSection = { Heading: string; CommandNames: string list }\n\n    let private rootHelpSections =\n        [\n            { Heading = \"Getting started\"; CommandNames = [ \"auth\"; \"connect\"; \"config\" ] }\n            {\n                Heading = \"Day-to-day development\"\n                CommandNames =\n                    [\n                        \"branch\"\n                        \"diff\"\n                        \"directory-version\"\n                        \"watch\"\n                    ]\n            }\n            {\n                Heading = \"Review and promotion\"\n                CommandNames =\n                    [\n                        \"workitem\"\n                        \"review\"\n                        \"candidate\"\n                        \"queue\"\n                        \"promotion-set\"\n                        \"agent\"\n                    ]\n            }\n            {\n                Heading = \"Administration and access\"\n                CommandNames =\n                    [\n                        \"owner\"\n                        \"organization\"\n                        \"repository\"\n                        \"access\"\n                        \"admin\"\n                    ]\n            }\n            { Heading = \"Local utilities\"; CommandNames = [ \"history\"; \"maintenance\"; \"alias\" ] }\n        ]\n\n    let private repositoryHelpSections =\n        [\n            { Heading = \"Create and initialize\"; CommandNames = [ \"create\"; \"init\" ] }\n            { Heading = \"Inspect\"; CommandNames = [ \"get\"; \"get-branches\" ] }\n            {\n                Heading = \"Configuration\"\n                CommandNames =\n                    [\n                        \"set-visibility\"\n                        \"set-status\"\n                        \"set-anonymous-access\"\n                        \"set-allows-large-files\"\n                        \"set-record-saves\"\n                        \"set-default-server-api-version\"\n                        \"set-save-days\"\n                        \"set-checkpoint-days\"\n                        \"set-diff-cache-days\"\n                        \"set-directory-version-cache-days\"\n                        \"set-logical-delete-days\"\n                        \"set-name\"\n                        \"set-description\"\n                        \"set-conflict-resolution-policy\"\n                    ]\n            }\n            { Heading = \"Lifecycle\"; CommandNames = [ \"delete\"; \"undelete\" ] }\n        ]\n\n    let private branchHelpSections =\n        [\n            {\n                Heading = \"Create and contribute\"\n                CommandNames =\n                    [\n                        \"create\"\n                        \"commit\"\n                        \"checkpoint\"\n                        \"save\"\n                        \"tag\"\n                        \"create-external\"\n                    ]\n            }\n            { Heading = \"Promotion workflow\"; CommandNames = [ \"promote\"; \"assign\"; \"rebase\" ] }\n            {\n                Heading = \"Inspect\"\n                CommandNames =\n                    [\n                        \"status\"\n                        \"list-contents\"\n                        \"get-recursive-size\"\n                        \"get\"\n                        \"get-references\"\n                        \"get-promotions\"\n                        \"get-commits\"\n                        \"get-checkpoints\"\n                        \"get-saves\"\n                        \"get-tags\"\n                        \"get-externals\"\n                    ]\n            }\n            {\n                Heading = \"Settings\"\n                CommandNames =\n                    [\n                        \"enable-assign\"\n                        \"enable-promotion\"\n                        \"enable-commit\"\n                        \"enable-checkpoints\"\n                        \"enable-save\"\n                        \"enable-tag\"\n                        \"enable-external\"\n                        \"enable-auto-rebase\"\n                        \"set-promotion-mode\"\n                        \"set-name\"\n                        \"update-parent-branch\"\n                    ]\n            }\n            { Heading = \"Lifecycle\"; CommandNames = [ \"delete\" ] }\n        ]\n\n    let private ownerHelpSections =\n        [\n            { Heading = \"Create and inspect\"; CommandNames = [ \"create\"; \"get\" ] }\n            {\n                Heading = \"Settings\"\n                CommandNames =\n                    [\n                        \"set-name\"\n                        \"set-type\"\n                        \"set-search-visibility\"\n                        \"set-description\"\n                    ]\n            }\n            { Heading = \"Lifecycle\"; CommandNames = [ \"delete\"; \"undelete\" ] }\n        ]\n\n    let private organizationHelpSections =\n        [\n            { Heading = \"Create and inspect\"; CommandNames = [ \"create\"; \"get\" ] }\n            {\n                Heading = \"Settings\"\n                CommandNames =\n                    [\n                        \"set-name\"\n                        \"set-type\"\n                        \"set-search-visibility\"\n                        \"set-description\"\n                    ]\n            }\n            { Heading = \"Lifecycle\"; CommandNames = [ \"delete\"; \"undelete\" ] }\n        ]\n\n    let private workItemHelpSections =\n        [\n            { Heading = \"Create and update\"; CommandNames = [ \"create\"; \"show\"; \"status\" ] }\n            {\n                Heading = \"Link and attach\"\n                CommandNames =\n                    [\n                        \"link\"\n                        \"attach\"\n                        \"attachments\"\n                        \"links\"\n                    ]\n            }\n        ]\n\n    let private groupedHelpSectionsByCommandName =\n        let lookup = Dictionary<string, HelpSection list>(StringComparer.InvariantCultureIgnoreCase)\n        lookup[\"repository\"] <- repositoryHelpSections\n        lookup[\"repo\"] <- repositoryHelpSections\n        lookup[\"branch\"] <- branchHelpSections\n        lookup[\"br\"] <- branchHelpSections\n        lookup[\"owner\"] <- ownerHelpSections\n        lookup[\"organization\"] <- organizationHelpSections\n        lookup[\"org\"] <- organizationHelpSections\n        lookup[\"workitem\"] <- workItemHelpSections\n        lookup[\"work\"] <- workItemHelpSections\n        lookup[\"work-item\"] <- workItemHelpSections\n        lookup[\"wi\"] <- workItemHelpSections\n        lookup\n\n    let private formatDisplayName (command: Command) =\n        let aliases =\n            command.Aliases\n            |> Seq.filter (fun alias ->\n                not\n                <| alias.Equals(command.Name, StringComparison.InvariantCultureIgnoreCase))\n            |> Seq.distinctBy (fun alias -> alias.ToLowerInvariant())\n            |> Seq.toArray\n\n        if aliases.Length = 0 then\n            command.Name\n        else\n            let aliasText = String.Join(\", \", aliases)\n            $\"{command.Name} ({aliasText})\"\n\n    let private getGroupedCommands (command: Command) (sections: HelpSection list) =\n        let lookup = Dictionary<string, Command>(StringComparer.InvariantCultureIgnoreCase)\n\n        command.Subcommands\n        |> Seq.cast<Command>\n        |> Seq.iter (fun command -> lookup[command.Name] <- command)\n\n        let mappedNames = HashSet<string>(StringComparer.InvariantCultureIgnoreCase)\n\n        let sections =\n            sections\n            |> List.choose (fun section ->\n                let commands =\n                    section.CommandNames\n                    |> List.choose (fun name ->\n                        match lookup.TryGetValue(name) with\n                        | true, command ->\n                            mappedNames.Add(command.Name) |> ignore\n                            Some command\n                        | _ -> None)\n\n                if commands.IsEmpty then None else Some(section.Heading, commands))\n\n        let unmapped =\n            command.Subcommands\n            |> Seq.cast<Command>\n            |> Seq.filter (fun command -> not <| mappedNames.Contains(command.Name))\n            |> Seq.sortBy (fun command -> command.Name)\n            |> Seq.toList\n\n        sections, unmapped\n\n    let private buildGroupedCommandLines (command: Command) (sections: HelpSection list) (indent: string) =\n        let sections, unmapped = getGroupedCommands command sections\n        let allCommands = (sections |> List.collect snd) @ unmapped\n\n        let maxNameWidth =\n            allCommands\n            |> Seq.map formatDisplayName\n            |> Seq.fold (fun current name -> max current name.Length) 0\n\n        let lines = ResizeArray<string>()\n        lines.Add($\"{indent}Commands:\")\n        lines.Add(String.Empty)\n\n        let writeSection heading commands =\n            lines.Add($\"{indent}  {heading}:\")\n\n            for command in commands do\n                let name = formatDisplayName command\n                let description = command.Description\n\n                if String.IsNullOrWhiteSpace(description) then\n                    lines.Add($\"{indent}    {name}\")\n                else\n                    lines.Add($\"{indent}    {name.PadRight(maxNameWidth)}  {description}\")\n\n            lines.Add(String.Empty)\n\n        for (heading, commands) in sections do\n            writeSection heading commands\n\n        if not unmapped.IsEmpty then writeSection \"Other\" unmapped\n\n        lines.ToArray()\n\n    let private stripAnsi (text: string) =\n        if String.IsNullOrEmpty(text) then\n            text\n        else\n            let withoutCsi = Regex.Replace(text, \"\\x1B\\\\[[0-9;?]*[A-Za-z]\", String.Empty)\n            Regex.Replace(withoutCsi, \"\\x1B\\\\][^\\x07]*\\x07\", String.Empty)\n\n    let private rewriteHelpCommands (helpText: string) (command: Command) (sections: HelpSection list) =\n        if command.Subcommands.Count = 0 then\n            helpText\n        else\n            let normalizedHelpText = helpText.Replace(\"\\r\\n\", \"\\n\")\n            let lines = normalizedHelpText.Split('\\n')\n\n            let commandHeaderIndex =\n                lines\n                |> Array.tryFindIndex (fun line ->\n                    let cleanedLine = stripAnsi line\n\n                    cleanedLine\n                        .Trim()\n                        .Equals(\"Commands:\", StringComparison.Ordinal))\n\n            match commandHeaderIndex with\n            | None -> helpText\n            | Some startIndex ->\n                let mutable endIndex = lines.Length\n                let mutable i = startIndex + 1\n\n                while i < lines.Length && endIndex = lines.Length do\n                    let line = stripAnsi lines[i]\n\n                    if\n                        (not <| String.IsNullOrWhiteSpace(line))\n                        && not (line.StartsWith(\" \"))\n                    then\n                        endIndex <- i\n                    else\n                        i <- i + 1\n\n                let before = if startIndex > 0 then lines[0 .. startIndex - 1] else Array.empty\n\n                let after = if endIndex < lines.Length then lines[endIndex..] else Array.empty\n\n                let headerLine = stripAnsi lines[startIndex]\n\n                let indentLength = headerLine.IndexOf(\"Commands:\", StringComparison.Ordinal)\n\n                let indent = if indentLength > 0 then headerLine.Substring(0, indentLength) else String.Empty\n\n                let grouped = buildGroupedCommandLines command sections indent\n\n                String.Join(Environment.NewLine, Array.concat [ before; grouped; after ])\n\n    let private findTargetHelpCommandResult (commandResult: CommandResult) =\n        let mutable current = commandResult\n        let mutable searching = true\n\n        while searching do\n            match current.Children.OfType<CommandResult>()\n                  |> Seq.tryLast\n                with\n            | Some childResult -> current <- childResult\n            | None -> searching <- false\n\n        current\n\n    let rootCommand =\n        // Create the root of the command tree.\n        let rootCommand = new RootCommand(\"Grace Version Control System\")\n\n        // Turning this off means getting much more flexible in our input handling, and that's not happening for a while.\n        rootCommand.TreatUnmatchedTokensAsErrors <- true\n\n        // Create global options - these appear on every command in the system.\n        rootCommand.Options.Add(Options.correlationId)\n        rootCommand.Options.Add(Options.source)\n        rootCommand.Options.Add(Options.output)\n\n        // Add subcommands.\n        rootCommand.Subcommands.Add(Connect.Build)\n        rootCommand.Subcommands.Add(Watch.Build)\n        rootCommand.Subcommands.Add(Branch.Build)\n        rootCommand.Subcommands.Add(DirectoryVersion.Build)\n        rootCommand.Subcommands.Add(Diff.Build)\n        rootCommand.Subcommands.Add(Repository.Build)\n        rootCommand.Subcommands.Add(Organization.Build)\n        rootCommand.Subcommands.Add(Owner.Build)\n        rootCommand.Subcommands.Add(Config.Build)\n        rootCommand.Subcommands.Add(History.Build)\n        rootCommand.Subcommands.Add(Auth.Build)\n        rootCommand.Subcommands.Add(Maintenance.Build)\n        rootCommand.Subcommands.Add(WorkItemCommand.Build)\n        rootCommand.Subcommands.Add(ReviewCommand.Build)\n        rootCommand.Subcommands.Add(CandidateCommand.Build)\n        rootCommand.Subcommands.Add(QueueCommand.Build)\n        rootCommand.Subcommands.Add(PromotionSetCommand.Build)\n        rootCommand.Subcommands.Add(AgentCommand.Build)\n        rootCommand.Subcommands.Add(Admin.Build)\n        rootCommand.Subcommands.Add(Access.Build)\n\n        let Alias = Command(\"alias\", \"Display aliases for Grace commands.\")\n        let ListAliases = Command(\"list\", \"Display aliases for Grace commands.\")\n        ListAliases.SetAction(fun _ -> printAliases ())\n        Alias.Subcommands.Add(ListAliases)\n        rootCommand.Subcommands.Add(Alias)\n        rootCommand\n\n    ///// Converts tokens to the exact casing defined in their options, enabling case-insensitive parsing on Windows.\n    //let caseInsensitiveMiddleware (rootCommand: Command, isCaseInsensitive) =\n    //    if isCaseInsensitive then\n    //        let parseResult = rootCommand.Parse()\n    //        let commandOptions = rootCommand.Options\n\n    //        // Collect all of the aliases for all of the options.\n    //        let allAliases = commandOptions |> Seq.collect (fun option -> option.Aliases)\n\n    //        /// Finds tokens that are either aliases for options, or pre-defined valid values for an option (i.e. completions), and converts them to the exact case found in the option definitions.\n    //        let getCorrectTokenCase (tokens: string array) =\n    //            /// The case-corrected list of tokens.\n    //            let newTokens = List<string>(tokens.Length)\n\n    //            // Loop through every token.\n    //            for i = 0 to (tokens.Length - 1) do\n    //                let token = tokens[i]\n    //                // First, check if this is an alias for an option.\n    //                match\n    //                    allAliases\n    //                    |> Seq.tryFind (fun alias -> alias.Equals(token, StringComparison.InvariantCultureIgnoreCase))\n    //                with\n    //                | Some alias ->\n    //                    // We found an alias, so we'll use the alias from the option definition to get the case exactly right.\n    //                    newTokens.Add(alias)\n    //                | None ->\n    //                    // This is either a value, or just an invalid option we didn't find.\n    //                    // If it's a known value (i.e. completion) for an option, we want to use the completion value defined in the option to get the case right.\n    //                    // If we don't recognize it, we'll just leave it as-is.\n    //                    if i > 0 then\n    //                        // Check if the previous token is an option. If it is, there's a chance it has completions.\n    //                        let previousToken = tokens[i - 1]\n\n    //                        match\n    //                            commandOptions\n    //                            |> Seq.tryFind (fun option -> option.Aliases.Contains(previousToken, StringComparer.InvariantCultureIgnoreCase))\n    //                        with\n    //                        | Some option ->\n    //                            // We found an option for the previous token, so we'll check if it has completions.\n    //                            let dlk = Completions.CompletionContext.Empty\n\n    //                            let completions =\n    //                                option.GetCompletions(parseResult.GetCompletionContext())\n    //                                |> Seq.map (fun completion -> completion.InsertText)\n    //                                |> Seq.toArray\n\n    //                            // If we have completions, and this token is one of them, we'll use the actual completion value to get the case right.\n    //                            if completions.Length > 0 then\n    //                                newTokens.Add(\n    //                                    completions.FirstOrDefault(\n    //                                        (fun completion -> token.Equals(completion, StringComparison.InvariantCultureIgnoreCase)),\n    //                                        token\n    //                                    )\n    //                                )\n    //                            else\n    //                                // There are no completions, so we'll just use the current token as-is.\n    //                                newTokens.Add(token)\n    //                        | None ->\n    //                            // We didn't find an option for the previous token, so we'll just use the current token as-is.\n    //                            newTokens.Add(token)\n    //                    else\n    //                        // This is the first token, so we'll just use it as-is. If this were a valid option, we would have matched it and returned `Some alias`.\n    //                        newTokens.Add(token)\n\n    //            newTokens.ToArray()\n\n    //        // Get the text from all tokens on the command-line into an array.\n    //        let tokens = parseResult.Tokens.Select(fun token -> token.Value).ToArray()\n\n    //        // Convert the tokens to the correct case.\n    //        let newTokens =\n    //            match tokens.Count with\n    //            | 0 ->\n    //                // There are no tokens, so we'll just return an empty array.\n    //                Array.empty<string>\n    //            | 1 ->\n    //                // This could be a command like `grace branch`, which is a valid request for help. I'll convert the token to lower-case to match Grace's command structure.\n    //                [| tokens[0].ToLowerInvariant() |]\n    //            | _ ->\n    //                // We've already converted aliases to their noun-verb form, so we know the first two tokens should be converted to lower-case to match Grace's command structure.\n    //                // The rest of the tokens are either options or values, and will be converted to their exact casing based on the option definitions.\n\n    //                // In case someone typed `grace --some-option blah`, which is invalid, I want to leave it alone.\n    //                if tokens[0].StartsWith(\"-\") then\n    //                    tokens\n    //                else\n    //                    // Convert the first two tokens to lower-case.\n    //                    //Array.append [| tokens[0].ToLowerInvariant(); tokens[1].ToLowerInvariant() |] (getCorrectTokenCase tokens[2..])\n    //                    Array.append [| tokens[0].ToLowerInvariant() |] (getCorrectTokenCase tokens[1..])\n\n    //        // Replace the old ParseResult with one based on the updated tokens with exact casing.\n    //        rootCommand.Parse(newTokens)\n    //    else\n    //        rootCommand.Parse()\n\n    /// Checks if the command is a `grace watch` command.\n    let isGraceWatch (parseResult: ParseResult) = if (parseResult.CommandResult.Command.Name = \"watch\") then true else false\n\n    /// Checks if the command is a `grace config` command.\n    let isGraceConfig (parseResult: ParseResult) =\n        if not <| isNull parseResult.CommandResult.Parent then\n            if parseResult.CommandResult.Parent.GetValue(\"config\") then true else false\n        else\n            false\n\n    type FeedbackSection(action: HelpAction) =\n        inherit SynchronousCommandLineAction()\n\n        member _.HelpAction = action\n\n        override _.Invoke(parseResult: ParseResult) =\n            let result = action.Invoke(parseResult)\n            AnsiConsole.WriteLine()\n            AnsiConsole.WriteLine(\"More help and feedback:\")\n\n            AnsiConsole.WriteLine(\n                \"  For more help, or to give us feedback, please join us in Discussions, or create an issue in our repo, at https://github.com/scottarbeit/grace.\"\n            )\n\n            result\n\n    /// This is the main entry point for Grace CLI.\n    [<EntryPoint>]\n    let main args =\n        let startTime = getCurrentInstant ()\n        Auth.configureSdkAuth ()\n        Services.resetInvocationCorrelationId ()\n\n        // Create a MemoryCache instance.\n        //let memoryCacheOptions = MemoryCacheOptions(TrackStatistics = false, TrackLinkedCacheEntries = false)\n        //memoryCache <- new MemoryCache(memoryCacheOptions)\n\n        /// True if the OS is case-insensitive (i.e. Windows), false otherwise.\n        let isCaseInsensitive = Environment.OSVersion.Platform = PlatformID.Win32NT\n        let argvOriginal = args |> Array.copy\n\n        let normalizeArgsForHistory (args: string array) =\n            if args.Length = 0 then\n                Array.empty<string>\n            else\n                let firstToken = if isCaseInsensitive then args[ 0 ].ToLowerInvariant() else args[0]\n\n                let properCasedArgs =\n                    args\n                    |> Array.map (fun arg ->\n                        if isCaseInsensitive && arg.StartsWith(\"--\") then\n                            arg.ToLowerInvariant()\n                        else\n                            arg)\n\n                if aliases.ContainsKey(firstToken) then\n                    let newArgs = List<string>()\n\n                    for token in aliases[ firstToken ].Reverse() do\n                        newArgs.Insert(0, token)\n\n                    for token in properCasedArgs[1..] do\n                        newArgs.Add(token)\n\n                    newArgs.ToArray()\n                else\n                    properCasedArgs\n\n        let mutable argvNormalized = normalizeArgsForHistory args\n\n        (task {\n            let mutable parseResult: ParseResult = null\n            let mutable returnValue: int = 0\n            let mutable parseSucceeded: bool = false\n\n            try\n                try\n                    //let commandLineConfiguration = ParserConfiguration rootCommand\n\n                    let argvToParse = if args.Length = 0 then [| helpOptions[0] |] else argvNormalized\n\n                    parseResult <- rootCommand.Parse(argvToParse)\n                    parseSucceeded <- parseResult.Errors.Count = 0\n                    // Write the ParseResult to Services as global context for the CLI.\n                    Services.parseResult <- parseResult\n                    LocalStateDb.setVerbose (parseResult |> verbose)\n\n                    let helpAction =\n                        match parseResult.Action with\n                        | :? HelpAction as action -> Some action\n                        | :? FeedbackSection as feedback -> Some feedback.HelpAction\n                        | _ -> None\n\n                    let hasHelpToken =\n                        argvNormalized\n                        |> Array.takeWhile (fun arg -> not <| arg.Equals(\"--\", StringComparison.Ordinal))\n                        |> Array.exists (fun arg -> helpOptions.Contains(arg))\n\n                    let isHelpRequest =\n                        helpAction.IsSome\n                        || args.Length = 0\n                        || hasHelpToken\n\n                    if isHelpRequest then\n                        let helpCommandResult = findTargetHelpCommandResult parseResult.CommandResult\n                        let helpCommand = helpCommandResult.Command\n\n                        let isRootHelp =\n                            obj.ReferenceEquals(helpCommand, rootCommand)\n                            || helpCommand :? RootCommand\n\n                        let groupedHelpSections =\n                            if isRootHelp then\n                                Some rootHelpSections\n                            else\n                                match groupedHelpSectionsByCommandName.TryGetValue(helpCommand.Name) with\n                                | true, sections -> Some sections\n                                | _ -> None\n\n                        let groupedHelpCommand, groupedHelpSections =\n                            match groupedHelpSections with\n                            | Some sections -> helpCommand, Some sections\n                            | None ->\n                                match tryGetTopLevelCommandFromArgs argvNormalized isCaseInsensitive with\n                                | Some commandName ->\n                                    match groupedHelpSectionsByCommandName.TryGetValue(commandName) with\n                                    | true, sections ->\n                                        let comparison =\n                                            if isCaseInsensitive then\n                                                StringComparison.InvariantCultureIgnoreCase\n                                            else\n                                                StringComparison.InvariantCulture\n\n                                        let command =\n                                            rootCommand.Subcommands\n                                            |> Seq.cast<Command>\n                                            |> Seq.tryFind (fun command ->\n                                                command.Name.Equals(commandName, comparison)\n                                                || command.Aliases\n                                                   |> Seq.exists (fun alias -> alias.Equals(commandName, comparison)))\n\n                                        match command with\n                                        | Some command -> command, Some sections\n                                        | None -> helpCommand, None\n                                    | _ -> helpCommand, None\n                                | None -> helpCommand, None\n\n                        if isRootHelp\n                           && (parseResult.Tokens.Count = 0\n                               || (parseResult.Tokens.Count = 1\n                                   && helpOptions.Contains(parseResult.Tokens[0].Value))) then\n                            // This is where we configure how help is displayed by Grace.\n                            // We want to show the help text for the command, and then the feedback section at the end.\n                            let graceFiglet = FigletText($\"Grace\")\n                            graceFiglet.Justification <- Justify.Center\n                            graceFiglet.Color <- Color.Green3_1\n                            AnsiConsole.Write(graceFiglet)\n                            let graceFiglet = FigletText($\"Version Control System\")\n                            graceFiglet.Justification <- Justify.Center\n                            graceFiglet.Color <- Color.DarkOrange\n                            AnsiConsole.Write(graceFiglet)\n                            AnsiConsole.WriteLine()\n\n                            for i in 0 .. rootCommand.Options.Count - 1 do\n                                match rootCommand.Options[i] with\n                                | :? HelpOption as defaultHelpOption -> defaultHelpOption.Action <- FeedbackSection(defaultHelpOption.Action :?> HelpAction)\n                                | _ -> ()\n\n                        //helpAction.Builder.CustomizeLayout(fun layoutContext ->\n                        //    HelpBuilder.Default\n                        //        .GetLayout()\n                        //        .Where(fun section ->\n                        //            not\n                        //            <| section.Method.Name.Contains(\"Synopsis\", StringComparison.InvariantCultureIgnoreCase))\n                        //        .Append(feedbackSection))\n\n                        // We're passing a new List<Option> here, because we're going to be adding to it recursively in gatherAllOptions.\n                        let allOptions = gatherAllOptions helpCommand (List<Option>())\n\n                        // This section sets the display of the default value for these options in all commands in Grace CLI.\n                        // Without setting the display values here, by default, we'd get something like\n                        //   \"[default: thing-we-said-in-the-Option-definition] [default:e4def31b-4547-4f6b-9324-56eba666b4b2]\"\n                        //   i.e. whatever the generated Guid value on create might be.\n                        let optionsToUpdate =\n                            [\n                                {\n                                    optionAlias = OptionName.CorrelationId\n                                    display = \"new NanoId\"\n                                    displayOnCreate = \"new NanoId\"\n                                    createParentCommand = String.Empty\n                                }\n                                { optionAlias = OptionName.OwnerId; display = \"current OwnerId\"; displayOnCreate = \"new Guid\"; createParentCommand = \"owner\" }\n                                {\n                                    optionAlias = OptionName.OrganizationId\n                                    display = \"current OrganizationId\"\n                                    displayOnCreate = \"new Guid\"\n                                    createParentCommand = \"organization\"\n                                }\n                                {\n                                    optionAlias = OptionName.RepositoryId\n                                    display = \"current RepositoryId\"\n                                    displayOnCreate = \"new Guid\"\n                                    createParentCommand = \"repository\"\n                                }\n                                {\n                                    optionAlias = OptionName.BranchId\n                                    display = \"current BranchId\"\n                                    displayOnCreate = \"new Guid\"\n                                    createParentCommand = \"branch\"\n                                }\n                            ]\n\n                        let isCreate = helpCommand.Name.Equals(\"create\", StringComparison.OrdinalIgnoreCase)\n\n                        let parentCommands = helpCommand.Parents.OfType<Command>()\n\n                        let defaultsByAlias =\n                            optionsToUpdate\n                            |> Seq.map (fun optionToUpdate ->\n                                let useCreateDisplay =\n                                    isCreate\n                                    && not\n                                       <| String.IsNullOrWhiteSpace(optionToUpdate.createParentCommand)\n                                    && parentCommands.Any (fun parent ->\n                                        parent.Name.Equals(optionToUpdate.createParentCommand, StringComparison.OrdinalIgnoreCase))\n\n                                let defaultValueText =\n                                    if useCreateDisplay then\n                                        optionToUpdate.displayOnCreate\n                                    else\n                                        optionToUpdate.display\n\n                                optionToUpdate.optionAlias, defaultValueText)\n                            |> dict\n\n                        use writer = new StringWriter()\n                        let originalOut = Console.Out\n\n                        let invokeResult =\n                            try\n                                Console.SetOut(writer)\n                                parseResult.Invoke()\n                            finally\n                                Console.SetOut(originalOut)\n\n                        let helpText = writer.ToString()\n                        let rewrittenHelpText = rewriteHelpDefaults helpText defaultsByAlias\n\n                        let finalHelpText =\n                            match groupedHelpSections with\n                            | Some sections -> rewriteHelpCommands rewrittenHelpText groupedHelpCommand sections\n                            | None -> rewrittenHelpText\n\n                        Console.Write(finalHelpText)\n                        returnValue <- invokeResult\n                    else if configurationFileExists () then\n                        //parseResult <- caseInsensitiveMiddleware (rootCommand, parseResult, isCaseInsensitive)\n\n                        if parseResult |> hasOutput then\n                            if parseResult |> verbose then\n                                AnsiConsole.Write(\n                                    (new Rule($\"[{Colors.Important}]Started: {formatInstantExtended startTime}.[/]\"))\n                                        .RightJustified()\n                                )\n\n                            //printParseResult parseResult\n                            else\n                                AnsiConsole.Write(new Rule())\n\n                        // If this instance isn't `grace watch`, we want to check if `grace watch` is running by trying to read the IPC file.\n                        if not <| (parseResult |> isGraceWatch) then\n                            let! graceWatchStatus = getGraceWatchStatus ()\n\n                            match graceWatchStatus with\n                            | Some status -> Configuration.updateConfiguration { GraceWatchStatus = status }\n                            | None -> ()\n\n                        // Now we can invoke the command!\n                        let! returnValue = parseResult.InvokeAsync()\n\n                        // Stuff to do after the command has been invoked:\n\n                        // If this instance is `grace watch`, we'll actually delete the IPC file in the finally clause below, but\n                        //   we'll write the \"we deleted the file\" message to the console here, so it comes before the last Rule() is written.\n                        if parseResult |> isGraceWatch then\n                            logToAnsiConsole Colors.Important (getLocalizedString StringResourceName.InterprocessFileDeleted)\n\n                        // If we're writing output, write the final Rule() to the console.\n                        if parseResult |> hasOutput then\n                            let finishTime = getCurrentInstant ()\n\n                            let elapsed =\n                                (finishTime - startTime)\n                                    .Plus(Duration.FromMilliseconds(110.0)) // Adding 110ms for .NET Runtime startup time.\n\n                            if parseResult |> verbose then\n                                AnsiConsole.Write(\n                                    (new Rule(\n                                        $\"[{Colors.Important}]Elapsed: {elapsed.TotalSeconds:F3}s. Exit code: {returnValue}. Finished: {formatInstantExtended finishTime}[/]\"\n                                    ))\n                                        .RightJustified()\n                                )\n                            else\n                                AnsiConsole.Write(\n                                    (new Rule($\"[{Colors.Important}]Elapsed: {elapsed.TotalSeconds:F3}s. Exit code: {returnValue}.[/]\"))\n                                        .RightJustified()\n                                )\n\n                            AnsiConsole.WriteLine()\n                    else\n                        // We don't have a config file, so write an error message and exit.\n                        AnsiConsole.Write(new Rule())\n\n                        let comparison =\n                            if isCaseInsensitive then\n                                StringComparison.InvariantCultureIgnoreCase\n                            else\n                                StringComparison.InvariantCulture\n\n                        let allowedCommands =\n                            [\n                                \"config\"\n                                \"history\"\n                                \"auth\"\n                                \"connect\"\n                            ]\n\n                        let isAllowed =\n                            let command = parseResult.CommandResult.Command\n\n                            Seq.append [ command ] (command.Parents.OfType<Command>())\n                            |> Seq.exists (fun cmd ->\n                                allowedCommands\n                                |> List.exists (fun allowed -> cmd.Name.Equals(allowed, comparison)))\n                            || (tryGetTopLevelCommandFromArgs argvNormalized isCaseInsensitive\n                                |> Option.exists (fun topLevel ->\n                                    allowedCommands\n                                    |> List.exists (fun allowed -> topLevel.Equals(allowed, comparison))))\n\n                        if isAllowed then\n                            let! invokedReturnValue = parseResult.InvokeAsync()\n                            returnValue <- invokedReturnValue\n                            ()\n                        else\n                            AnsiConsole.MarkupLine($\"[{Colors.Important}]{getLocalizedString StringResourceName.GraceConfigFileNotFound}[/]\")\n\n                        let finishTime = getCurrentInstant ()\n\n                        let elapsed =\n                            (finishTime - startTime)\n                                .Plus(Duration.FromMilliseconds(110.0)) // Adding 110ms for .NET Runtime startup time.\n\n                        AnsiConsole.Write(\n                            (new Rule($\"[{Colors.Important}]Elapsed: {elapsed.TotalSeconds:F3}s. Exit code: {returnValue}.[/]\"))\n                                .RightJustified()\n                        )\n\n                    return returnValue\n                with\n                | ex ->\n                    AnsiConsole.WriteException ex\n                    //logToAnsiConsole Colors.Error $\"ex.Message: {ex.Message}\"\n                    //logToAnsiConsole Colors.Error $\"{ex.StackTrace}\"\n                    returnValue <- -1\n                    return -1\n            finally\n                let finishTime = getCurrentInstant ()\n\n                let durationMs =\n                    (finishTime - startTime).TotalMilliseconds\n                    |> int64\n\n                HistoryStorage.tryRecordInvocation\n                    {\n                        argvOriginal = argvOriginal\n                        argvNormalized = argvNormalized\n                        cwd = Environment.CurrentDirectory\n                        exitCode = returnValue\n                        durationMs = durationMs\n                        parseSucceeded = parseSucceeded\n                        timestampUtc = startTime\n                        source = resolveInvocationSource parseResult\n                    }\n\n                // If this was grace watch, delete the inter-process communication file.\n                if not <| isNull (parseResult)\n                   && parseResult |> isGraceWatch then\n                    File.Delete(IpcFileName())\n        })\n            .Result\n"
  },
  {
    "path": "src/Grace.CLI/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"distributionName\": \"\"\n    },\n    \"grace diff commit\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"diff commit\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User01\"\n    },\n    \"grace merge - PlayWithFlexbox\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"merge -m \\\"Merge from Debug\\\"\",\n      \"workingDirectory\": \"D:\\\\Source\\\\PlayWithFlexbox\"\n    },\n    \"grace diff merge\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"diff merge\",\n      \"workingDirectory\": \"D:\\\\Source\\\\PlayWithFlexbox\"\n    },\n    \"grace owner create\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"owner create --output Verbose --ownerName Owner3F\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User01\"\n    },\n    \"grace refs -b main\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"refs -b main --output Verbose\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\Chris\"\n    },\n    \"grace (no parameters)\": {\n      \"commandName\": \"Project\",\n      \"workingDirectory\": \"D:\\\\Source\\\\grace\\\\src\"\n    },\n    \"grace organization create\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"org create --organizationName SomeOrganizationName\",\n      \"workingDirectory\": \"D:\\\\Source\\\\gracedemo\\\\Chris\"\n    },\n    \"grace maint update-index\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"maint update-index -o Verbose\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User01\"\n    },\n    \"grace repo set-checkpointdays\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"repo set-checkpointdays --checkpointDays 5.1 --output Verbose\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\Chris\"\n    },\n    \"grace repo create\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"repository create --ownerName \\\"Owner0009\\\" --organizationName \\\"Organization0009\\\" --repositoryName \\\"Repository0009\\\" --repositoryId 042dc71e-e7d7-4f7f-bdac-f930b398fa8e\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\Chris\"\n    },\n    \"grace rebase\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"rebase\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\Filip\"\n    },\n    \"grace branches\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"branches --output Verbose\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\Mika\"\n    },\n    \"grace config write\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"config write\",\n      \"workingDirectory\": \"C:\\\\Temp\"\n    },\n    \"grace owner get\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"owner get -o Verbose\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User01\"\n    },\n    \"grace org create\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"org create --output Verbose --ownerName Owner1C --organizationName Org9FEDc --organizationId 9871826b-73f4-41a4-bf4b-dea42ea95bf1\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User01\"\n    },\n    \"grace watch - Chris\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"watch\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\Scott\"\n    },\n    \"grace branch switch\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"branch switch -c Scott\",\n      \"workingDirectory\": \"D:\\\\Source\\\\GraceDemo\\\\User01\"\n    },\n    \"grace switch -c Scott\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"switch -c Scott -o Verbose\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User02\"\n    },\n    \"grace status\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"status\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User01\"\n    },\n    \"grace watch\": {\n      \"commandName\": \"Project\",\n      \"commandLineArgs\": \"watch\",\n      \"workingDirectory\": \"E:\\\\GraceDemo\\\\User02\"\n    }\n  }\n}"
  },
  {
    "path": "src/Grace.CLI/Text.CLI.fs",
    "content": "namespace Grace.CLI\n\nopen Grace.Shared.Resources.Utilities\nopen Grace.Shared.Resources.Text\n\nmodule Text =\n\n    module OptionName =\n        [<Literal>]\n        let AllowsLargeFiles = \"--allows-large-files\"\n\n        [<Literal>]\n        let AnonymousAccess = \"--anonymous-access\"\n\n        [<Literal>]\n        let BranchId = \"--branch-id\"\n\n        [<Literal>]\n        let BranchName = \"--branch-name\"\n\n        [<Literal>]\n        let CheckpointDays = \"--checkpoint-days\"\n\n        [<Literal>]\n        let CorrelationId = \"--correlation-id\"\n\n        [<Literal>]\n        let D1 = \"--d1\"\n\n        [<Literal>]\n        let D2 = \"--d2\"\n\n        [<Literal>]\n        let DefaultServerApiVersion = \"--default-server-api-version\"\n\n        [<Literal>]\n        let DeleteReason = \"--delete-reason\"\n\n        [<Literal>]\n        let Description = \"--description\"\n\n        [<Literal>]\n        let DiffCacheDays = \"--diff-cache-days\"\n\n        [<Literal>]\n        let Directory = \"--directory\"\n\n        [<Literal>]\n        let DirectoryVersionCacheDays = \"--directory-version-cache-days\"\n\n        [<Literal>]\n        let DirectoryVersionId = \"--directory-version-id\"\n\n        [<Literal>]\n        let DirectoryVersionId1 = \"--directory-version-id-1\"\n\n        [<Literal>]\n        let DirectoryVersionId2 = \"--directory-version-id-2\"\n\n        [<Literal>]\n        let DoNotSwitch = \"--do-not-switch\"\n\n        [<Literal>]\n        let Enabled = \"--enabled\"\n\n        [<Literal>]\n        let Force = \"--force\"\n\n        [<Literal>]\n        let ForceRecompute = \"--force-recompute\"\n\n        [<Literal>]\n        let FullSha = \"--full-sha\"\n\n        [<Literal>]\n        let GraceConfig = \"--grace-config\"\n\n        [<Literal>]\n        let IncludeDeleted = \"--include-deleted\"\n\n        [<Literal>]\n        let Individual = \"--individual\"\n\n        [<Literal>]\n        let InitialPermissions = \"--initial-permissions\"\n\n        [<Literal>]\n        let ListDirectories = \"--list-directories\"\n\n        [<Literal>]\n        let ListFiles = \"--list-files\"\n\n        [<Literal>]\n        let LogicalDeleteDays = \"--logical-delete-days\"\n\n        [<Literal>]\n        let Limit = \"--limit\"\n\n        [<Literal>]\n        let MaxCount = \"--max-count\"\n\n        [<Literal>]\n        let Message = \"--message\"\n\n        [<Literal>]\n        let Contains = \"--contains\"\n\n        [<Literal>]\n        let NewName = \"--new-name\"\n\n        [<Literal>]\n        let OrganizationId = \"--organization-id\"\n\n        [<Literal>]\n        let OrganizationName = \"--organization-name\"\n\n        [<Literal>]\n        let OrganizationType = \"--organization-type\"\n\n        [<Literal>]\n        let Output = \"--output\"\n\n        [<Literal>]\n        let Overwrite = \"--overwrite\"\n\n        [<Literal>]\n        let OwnerId = \"--owner-id\"\n\n        [<Literal>]\n        let OwnerName = \"--owner-name\"\n\n        [<Literal>]\n        let OwnerType = \"--owner-type\"\n\n        [<Literal>]\n        let ParentBranchId = \"--parent-branch-id\"\n\n        [<Literal>]\n        let ParentBranchName = \"--parent-branch-name\"\n\n        [<Literal>]\n        let Repo = \"--repo\"\n\n        [<Literal>]\n        let RecordSaves = \"--record-saves\"\n\n        [<Literal>]\n        let ReassignChildBranches = \"--reassign-child-branches\"\n\n        [<Literal>]\n        let NewParentBranchId = \"--new-parent-branch-id\"\n\n        [<Literal>]\n        let NewParentBranchName = \"--new-parent-branch-name\"\n\n        [<Literal>]\n        let ReferenceId = \"--reference-id\"\n\n        [<Literal>]\n        let ReferenceType = \"--reference-type\"\n\n        [<Literal>]\n        let RepositoryId = \"--repository-id\"\n\n        [<Literal>]\n        let RepositoryName = \"--repository-name\"\n\n        [<Literal>]\n        let RetrieveDefaultBranch = \"--retrieve-default-branch\"\n\n        [<Literal>]\n        let Claim = \"--claim\"\n\n        [<Literal>]\n        let DirectoryPermission = \"--dir-perm\"\n\n        [<Literal>]\n        let Operation = \"--operation\"\n\n        [<Literal>]\n        let Path = \"--path\"\n\n        [<Literal>]\n        let PrincipalId = \"--principal-id\"\n\n        [<Literal>]\n        let PrincipalType = \"--principal-type\"\n\n        [<Literal>]\n        let ResourceKind = \"--resource\"\n\n        [<Literal>]\n        let RoleId = \"--role\"\n\n        [<Literal>]\n        let SaveDays = \"--save-days\"\n\n        [<Literal>]\n        let Failed = \"--failed\"\n\n        [<Literal>]\n        let Success = \"--success\"\n\n        [<Literal>]\n        let SearchVisibility = \"--search-visibility\"\n\n        [<Literal>]\n        let ServerAddress = \"--server-address\"\n\n        [<Literal>]\n        let ScopeKind = \"--scope\"\n\n        [<Literal>]\n        let S1 = \"--s1\"\n\n        [<Literal>]\n        let S2 = \"--s2\"\n\n        [<Literal>]\n        let Sha256Hash = \"--sha256-hash\"\n\n        [<Literal>]\n        let Sha256Hash1 = \"--sha256-hash-1\"\n\n        [<Literal>]\n        let Sha256Hash2 = \"--sha256-hash-2\"\n\n        [<Literal>]\n        let ShowEvents = \"--show-events\"\n\n        [<Literal>]\n        let Source = \"--source\"\n\n        [<Literal>]\n        let SourceDetail = \"--source-detail\"\n\n        [<Literal>]\n        let Since = \"--since\"\n\n        [<Literal>]\n        let Status = \"--status\"\n\n        [<Literal>]\n        let Tag = \"--tag\"\n\n        [<Literal>]\n        let ToBranchId = \"--to-branch-id\"\n\n        [<Literal>]\n        let ToBranchName = \"--to-branch-name\"\n\n        [<Literal>]\n        let Visibility = \"--visibility\"\n\n        [<Literal>]\n        let Id = \"--id\"\n\n        [<Literal>]\n        let Yes = \"--yes\"\n\n        [<Literal>]\n        let DryRun = \"--dry-run\"\n\n        [<Literal>]\n        let UseCurrentCwd = \"--use-current-cwd\"\n\n        [<Literal>]\n        let Replace = \"--replace\"\n\n    /// The full list of strings that can be displayed to the user.\n    type UIString =\n        | CreatingNewDirectoryVersions\n        | CreatingSaveReference\n        | GettingCurrentBranch\n        | GettingLatestVersion\n        | ReadingGraceStatus\n        | SavingDirectoryVersions\n        | ScanningWorkingDirectory\n        | UpdatingWorkingDirectory\n        | UploadingFiles\n        | WritingGraceStatusFile\n\n        static member getString(uiString: UIString) : string =\n            match uiString with\n            | CreatingNewDirectoryVersions -> getLocalizedString StringResourceName.CreatingNewDirectoryVersions\n            | CreatingSaveReference -> getLocalizedString StringResourceName.CreatingSaveReference\n            | GettingCurrentBranch -> getLocalizedString StringResourceName.GettingCurrentBranch\n            | GettingLatestVersion -> getLocalizedString StringResourceName.GettingLatestVersion\n            | ReadingGraceStatus -> getLocalizedString StringResourceName.ReadingGraceStatus\n            | SavingDirectoryVersions -> getLocalizedString StringResourceName.SavingDirectoryVersions\n            | ScanningWorkingDirectory -> getLocalizedString StringResourceName.ScanningWorkingDirectory\n            | UpdatingWorkingDirectory -> getLocalizedString StringResourceName.UpdatingWorkingDirectory\n            | UploadingFiles -> getLocalizedString StringResourceName.UploadingFiles\n            | WritingGraceStatusFile -> getLocalizedString StringResourceName.WritingGraceStatusFile\n"
  },
  {
    "path": "src/Grace.CLI.LocalStateDb.Worker/Grace.CLI.LocalStateDb.Worker.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <LangVersion>preview</LangVersion>\n    <IsPackable>false</IsPackable>\n    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"Program.fs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Grace.CLI\\Grace.CLI.fsproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.CLI.LocalStateDb.Worker/Program.fs",
    "content": "namespace Grace.CLI.LocalStateDb.Worker\n\nopen System\nopen System.Threading.Tasks\nopen Grace.CLI\nopen Grace.Types.Types\nopen NodaTime\n\nmodule Program =\n    let private run (dbPath: string) (rootId: Guid) (rootHash: string) (iterations: int) =\n        task {\n            let baseTicks =\n                SystemClock\n                    .Instance\n                    .GetCurrentInstant()\n                    .ToUnixTimeTicks()\n\n            let mutable index = 0\n\n            while index < iterations do\n                let ticks = baseTicks + int64 index\n\n                let status =\n                    { GraceStatus.Default with\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootHash\n                        LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(ticks)\n                        LastSuccessfulDirectoryVersionUpload = Instant.FromUnixTimeTicks(ticks)\n                    }\n\n                do! LocalStateDb.applyStatusIncremental dbPath status Seq.empty Seq.empty\n                index <- index + 1\n        }\n\n    [<EntryPoint>]\n    let main argv =\n        try\n            if argv.Length < 4 then\n                Console.Error.WriteLine(\"Usage: <dbPath> <rootId> <rootHash> <iterations>\")\n                2\n            else\n                let dbPath = argv[0]\n                let rootId = Guid.Parse(argv[1])\n                let rootHash = argv[2]\n                let iterations = Int32.Parse(argv[3])\n\n                run dbPath rootId rootHash iterations\n                |> fun task -> task.GetAwaiter().GetResult()\n\n                0\n        with\n        | ex ->\n            Console.Error.WriteLine(ex.ToString())\n            1\n"
  },
  {
    "path": "src/Grace.CLI.Tests/AGENTS.md",
    "content": "# Grace.CLI.Tests Agents Guide\n\nRead `../AGENTS.md` for global expectations before updating CLI tests.\n\n## Purpose\n\n- Validate CLI command behavior and helpers without requiring server access.\n- Keep tests deterministic and focused on parsing, command routing, output shaping, and local history/config behavior.\n\n## Test File Organization\n\n- Match test files to CLI command files where practical:\n  - `Grace.CLI/Command/Agent.CLI.fs` -> `Grace.CLI.Tests/Agent.CLI.Tests.fs`\n  - `Grace.CLI/Command/PromotionSet.CLI.fs` -> `Grace.CLI.Tests/PromotionSet.CLI.Tests.fs`\n  - `Grace.CLI/Command/Queue.CLI.fs` -> `Grace.CLI.Tests/Queue.CLI.Tests.fs`\n  - `Grace.CLI/Command/Review.CLI.fs` -> `Grace.CLI.Tests/Review.CLI.Tests.fs`\n  - `Grace.CLI/Command/WorkItem.CLI.fs` -> `Grace.CLI.Tests/WorkItem.CLI.Tests.fs`\n  - Root command behavior belongs in `Grace.CLI.Tests/Program.CLI.Tests.fs`.\n- Keep auth-focused tests separate for now (`Auth.Tests.fs`, `AuthTokenBundle.Tests.fs`).\n\n## Key Patterns\n\n1. Keep tests isolated: back up and restore any files touched under `~/.grace`.\n2. Use FsUnit for assertions and FsCheck for property-based coverage when appropriate.\n3. Prefer deterministic timestamps for history-related tests.\n\n## Validation\n\n- Run `dotnet build --configuration Release` if needed.\n- Run `dotnet test --no-build --configuration Release` after changes.\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Agent.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.Shared.Client.Configuration\nopen NUnit.Framework\nopen System\nopen System.IO\n\n[<NonParallelizable>]\nmodule AgentCommandTests =\n    let private ownerId = Guid.NewGuid()\n    let private organizationId = Guid.NewGuid()\n    let private repositoryId = Guid.NewGuid()\n\n    let private withIds (args: string array) =\n        Array.append\n            args\n            [|\n                \"--owner-id\"\n                ownerId.ToString()\n                \"--organization-id\"\n                organizationId.ToString()\n                \"--repository-id\"\n                repositoryId.ToString()\n            |]\n\n    let private withIdsAndSilent (args: string array) =\n        args\n        |> Array.append [| \"--output\"; \"Silent\" |]\n        |> withIds\n\n    let private invoke (args: string array) =\n        let parseResult = GraceCommand.rootCommand.Parse(args)\n        parseResult.Invoke()\n\n    let private withTempDir (action: string -> unit) =\n        let tempDir = Path.Combine(Path.GetTempPath(), $\"grace-agent-tests-{Guid.NewGuid():N}\")\n        Directory.CreateDirectory(tempDir) |> ignore\n        let originalDir = Environment.CurrentDirectory\n\n        try\n            Environment.CurrentDirectory <- tempDir\n            resetConfiguration ()\n            action tempDir\n        finally\n            Environment.CurrentDirectory <- originalDir\n            resetConfiguration ()\n\n            if Directory.Exists(tempDir) then\n                try\n                    Directory.Delete(tempDir, true)\n                with\n                | _ -> ()\n\n    let private writeLocalState (root: string) (agentId: Guid) (sessionId: string) (workItemId: string) =\n        let graceDirectory = Path.Combine(root, \".grace\")\n\n        Directory.CreateDirectory(graceDirectory)\n        |> ignore\n\n        let json =\n            sprintf\n                \"\"\"{\n  \"AgentId\": \"%s\",\n  \"AgentDisplayName\": \"Codex\",\n  \"Source\": \"codex\",\n  \"ActiveSessionId\": \"%s\",\n  \"ActiveWorkItemIdOrNumber\": \"%s\",\n  \"ActivePromotionSetId\": \"\",\n  \"LastOperationId\": \"op-1\",\n  \"LastCorrelationId\": \"corr-1\",\n  \"LastUpdatedAtUtc\": \"2026-02-27T00:00:00Z\"\n}\"\"\"\n                (agentId.ToString())\n                sessionId\n                workItemId\n\n        File.WriteAllText(Path.Combine(graceDirectory, \"agent-session-state.json\"), json)\n\n    [<Test>]\n    let ``agent add-summary rejects invalid work item id`` () =\n        let missingSummary = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n\n        let exitCode =\n            invoke (\n                withIdsAndSilent [| \"agent\"\n                                    \"add-summary\"\n                                    \"--work-item-id\"\n                                    \"not-a-guid\"\n                                    \"--summary-file\"\n                                    missingSummary |]\n            )\n\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``agent add-summary rejects missing summary file`` () =\n        let missingSummary = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n\n        let exitCode =\n            invoke (\n                withIdsAndSilent [| \"agent\"\n                                    \"add-summary\"\n                                    \"--work-item-id\"\n                                    Guid.NewGuid().ToString()\n                                    \"--summary-file\"\n                                    missingSummary |]\n            )\n\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``agent add-summary accepts numeric work item identifier`` () =\n        let missingSummary = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIds [| \"agent\"\n                           \"add-summary\"\n                           \"--work-item-id\"\n                           \"123\"\n                           \"--summary-file\"\n                           missingSummary |]\n            )\n\n        parseResult.Errors.Count |> should equal 0\n\n    [<Test>]\n    let ``agent add-summary accepts guid work item identifier`` () =\n        let missingSummary = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIds [| \"agent\"\n                           \"add-summary\"\n                           \"--work-item-id\"\n                           (Guid.NewGuid().ToString())\n                           \"--summary-file\"\n                           missingSummary |]\n            )\n\n        parseResult.Errors.Count |> should equal 0\n\n    [<Test>]\n    let ``agent add-summary accepts prompt and promotion-set options with numeric identifier`` () =\n        let missingSummary = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n        let missingPrompt = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n        let promotionSetId = Guid.NewGuid().ToString()\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIds [| \"agent\"\n                           \"add-summary\"\n                           \"--work-item-id\"\n                           \"42\"\n                           \"--summary-file\"\n                           missingSummary\n                           \"--prompt-file\"\n                           missingPrompt\n                           \"--prompt-origin\"\n                           \"agent://codex\"\n                           \"--promotion-set-id\"\n                           promotionSetId |]\n            )\n\n        parseResult.Errors.Count |> should equal 0\n\n    [<Test>]\n    let ``agent add-summary rejects prompt-origin without prompt file`` () =\n        let summaryPath = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.md\")\n\n        try\n            File.WriteAllText(summaryPath, \"summary\")\n\n            let exitCode =\n                invoke (\n                    withIdsAndSilent [| \"agent\"\n                                        \"add-summary\"\n                                        \"--work-item-id\"\n                                        \"42\"\n                                        \"--summary-file\"\n                                        summaryPath\n                                        \"--prompt-origin\"\n                                        \"agent://codex\" |]\n                )\n\n            exitCode |> should equal -1\n        finally\n            if File.Exists(summaryPath) then File.Delete(summaryPath)\n\n    [<Test>]\n    let ``agent bootstrap succeeds without repository config`` () =\n        withTempDir (fun root ->\n            let agentId = Guid.NewGuid()\n\n            let exitCode =\n                invoke [| \"agent\"\n                          \"bootstrap\"\n                          \"--agent-id\"\n                          agentId.ToString()\n                          \"--display-name\"\n                          \"Codex\"\n                          \"--output\"\n                          \"Silent\" |]\n\n            exitCode |> should equal 0\n\n            File.Exists(Path.Combine(root, \".grace\", \"agent-session-state.json\"))\n            |> should equal true)\n\n    [<Test>]\n    let ``agent work start reports actionable missing config`` () =\n        withTempDir (fun _ ->\n            let exitCode =\n                invoke [| \"agent\"\n                          \"work\"\n                          \"start\"\n                          \"--work-item-id\"\n                          \"42\"\n                          \"--output\"\n                          \"Silent\" |]\n\n            exitCode |> should equal -1)\n\n    [<Test>]\n    let ``agent work start rejects stale local state mismatch`` () =\n        withTempDir (fun root ->\n            writeLocalState root (Guid.NewGuid()) \"session-1\" \"41\"\n\n            let exitCode =\n                invoke (\n                    withIdsAndSilent [| \"agent\"\n                                        \"work\"\n                                        \"start\"\n                                        \"--work-item-id\"\n                                        \"42\" |]\n                )\n\n            exitCode |> should equal -1)\n\n    [<Test>]\n    let ``agent work start handles idempotent local replay`` () =\n        withTempDir (fun root ->\n            writeLocalState root (Guid.NewGuid()) \"session-1\" \"42\"\n\n            let exitCode =\n                invoke (\n                    withIdsAndSilent [| \"agent\"\n                                        \"work\"\n                                        \"start\"\n                                        \"--work-item-id\"\n                                        \"42\" |]\n                )\n\n            exitCode |> should equal 0)\n\n    [<Test>]\n    let ``agent work stop is idempotent when no active local session exists`` () =\n        withTempDir (fun _ ->\n            let bootstrapExitCode =\n                invoke [| \"agent\"\n                          \"bootstrap\"\n                          \"--agent-id\"\n                          Guid.NewGuid().ToString()\n                          \"--display-name\"\n                          \"Codex\"\n                          \"--output\"\n                          \"Silent\" |]\n\n            bootstrapExitCode |> should equal 0\n\n            let stopExitCode =\n                invoke (\n                    withIdsAndSilent [| \"agent\"\n                                        \"work\"\n                                        \"stop\" |]\n                )\n\n            stopExitCode |> should equal 0)\n\n    [<Test>]\n    let ``agent work status rejects stale session override`` () =\n        withTempDir (fun root ->\n            writeLocalState root (Guid.NewGuid()) \"session-1\" \"42\"\n\n            let exitCode =\n                invoke (\n                    withIdsAndSilent [| \"agent\"\n                                        \"work\"\n                                        \"status\"\n                                        \"--session-id\"\n                                        \"session-2\" |]\n                )\n\n            exitCode |> should equal -1)\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Auth.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI.Command\nopen Grace.Shared\nopen Grace.Types\nopen NUnit.Framework\nopen System\n\n[<NonParallelizable>]\nmodule AuthTests =\n    let private withEnv (name: string) (value: string option) (action: unit -> unit) =\n        let original = Environment.GetEnvironmentVariable(name)\n\n        match value with\n        | Some v -> Environment.SetEnvironmentVariable(name, v)\n        | None -> Environment.SetEnvironmentVariable(name, null)\n\n        try\n            action ()\n        finally\n            Environment.SetEnvironmentVariable(name, original)\n\n    let private clearOidcEnv (action: unit -> unit) =\n        withEnv Constants.EnvironmentVariables.GraceAuthOidcAuthority None (fun () ->\n            withEnv Constants.EnvironmentVariables.GraceAuthOidcAudience None (fun () ->\n                withEnv Constants.EnvironmentVariables.GraceAuthOidcCliClientId None (fun () ->\n                    withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientId None (fun () ->\n                        withEnv Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret None (fun () ->\n                            withEnv Constants.EnvironmentVariables.GraceServerUri None action)))))\n\n    [<Test>]\n    let ``tryGetAccessToken returns Error when auth is not configured`` () =\n        clearOidcEnv (fun () ->\n            withEnv Constants.EnvironmentVariables.GraceToken None (fun () ->\n                let result = Auth.tryGetAccessToken().GetAwaiter().GetResult()\n\n                match result with\n                | Ok _ -> Assert.Fail(\"Expected Error for missing auth configuration.\")\n                | Error _ -> ()))\n\n    [<Test>]\n    let ``tryGetAccessToken prefers GRACE_TOKEN env var`` () =\n        let token = PersonalAccessToken.formatToken \"user-1\" (Guid.NewGuid()) (Array.zeroCreate 32)\n\n        clearOidcEnv (fun () ->\n            withEnv Constants.EnvironmentVariables.GraceToken (Some token) (fun () ->\n                let result = Auth.tryGetAccessToken().GetAwaiter().GetResult()\n\n                match result with\n                | Ok (Some value) -> value |> should equal token\n                | Ok None -> Assert.Fail(\"Expected GRACE_TOKEN to be returned.\")\n                | Error message -> Assert.Fail($\"Unexpected error: {message}\")))\n\n    [<Test>]\n    let ``tryGetAccessToken rejects invalid GRACE_TOKEN`` () =\n        clearOidcEnv (fun () ->\n            withEnv Constants.EnvironmentVariables.GraceToken (Some \"not-a-pat\") (fun () ->\n                let result = Auth.tryGetAccessToken().GetAwaiter().GetResult()\n\n                match result with\n                | Ok _ -> Assert.Fail(\"Expected Error for invalid GRACE_TOKEN.\")\n                | Error _ -> ()))\n"
  },
  {
    "path": "src/Grace.CLI.Tests/AuthTokenBundle.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen Grace.CLI.Command\nopen Grace.Shared\nopen NodaTime\nopen NUnit.Framework\nopen System.Text.Json\n\n[<Parallelizable(ParallelScope.All)>]\ntype AuthTokenBundleTests() =\n    [<Test>]\n    member _.TokenBundleRoundTrips() =\n        let now = Instant.FromUtc(2025, 12, 31, 0, 0)\n\n        let bundle: Auth.TokenBundle =\n            {\n                RefreshToken = \"refresh\"\n                AccessToken = \"access\"\n                AccessTokenExpiresAt = now.Plus(Duration.FromHours(1.0))\n                Issuer = \"https://tenant.us.auth0.com/\"\n                Audience = \"https://api.gracevcs.com\"\n                Scopes = \"openid profile email offline_access\"\n                Subject = Some \"user-1\"\n                ClientId = \"cli-client\"\n                CreatedAt = now\n                UpdatedAt = now\n            }\n\n        let json = JsonSerializer.Serialize(bundle, Constants.JsonSerializerOptions)\n        let roundTrip = JsonSerializer.Deserialize<Auth.TokenBundle>(json, Constants.JsonSerializerOptions)\n\n        Assert.That(roundTrip, Is.EqualTo(bundle))\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Connect.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.CLI.Command\nopen Grace.CLI.Text\nopen Grace.Shared.Client.Configuration\nopen Grace.Types.Branch\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen NUnit.Framework\nopen Spectre.Console\nopen System\nopen System.IO\n\n[<NonParallelizable>]\nmodule ConnectTests =\n    let private setAnsiConsoleOutput (writer: TextWriter) =\n        let settings = AnsiConsoleSettings()\n        settings.Out <- AnsiConsoleOutput(writer)\n        AnsiConsole.Console <- AnsiConsole.Create(settings)\n\n    let private runWithCapturedOutput (args: string array) =\n        use writer = new StringWriter()\n        let originalOut = Console.Out\n\n        try\n            Console.SetOut(writer)\n            setAnsiConsoleOutput writer\n            let exitCode = GraceCommand.main args\n            exitCode, writer.ToString()\n        finally\n            Console.SetOut(originalOut)\n            setAnsiConsoleOutput originalOut\n\n    let private withTempDir (action: string -> unit) =\n        let tempDir = Path.Combine(Path.GetTempPath(), $\"grace-cli-tests-{Guid.NewGuid():N}\")\n        Directory.CreateDirectory(tempDir) |> ignore\n        let originalDir = Environment.CurrentDirectory\n\n        try\n            Environment.CurrentDirectory <- tempDir\n            action tempDir\n        finally\n            Environment.CurrentDirectory <- originalDir\n\n            if Directory.Exists(tempDir) then\n                try\n                    Directory.Delete(tempDir, true)\n                with\n                | _ -> ()\n\n    let private getGraceConfigPath root = Path.Combine(root, \".grace\", \"graceconfig.json\")\n\n    [<Test>]\n    let ``connect creates config when missing`` () =\n        withTempDir (fun root ->\n            let exitCode, _ = runWithCapturedOutput [| \"connect\" |]\n            exitCode |> should equal -1\n\n            File.Exists(getGraceConfigPath root)\n            |> should equal true)\n\n    [<Test>]\n    let ``connect retrieve default branch defaults to true`` () =\n        let parseResult = GraceCommand.rootCommand.Parse([| \"connect\" |])\n\n        parseResult.GetValue<bool>(OptionName.RetrieveDefaultBranch)\n        |> should equal true\n\n    [<Test>]\n    let ``connect retrieve default branch parses explicit false`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                [|\n                    \"connect\"\n                    OptionName.RetrieveDefaultBranch\n                    \"false\"\n                |]\n            )\n\n        parseResult.GetValue<bool>(OptionName.RetrieveDefaultBranch)\n        |> should equal false\n\n    [<Test>]\n    let ``connect directory version selection precedence uses directory version id`` () =\n        let directoryVersionId = Guid.NewGuid()\n        let referenceId = Guid.NewGuid()\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                [|\n                    \"connect\"\n                    OptionName.DirectoryVersionId\n                    $\"{directoryVersionId}\"\n                    OptionName.ReferenceId\n                    $\"{referenceId}\"\n                    OptionName.ReferenceType\n                    \"Commit\"\n                |]\n            )\n\n        match Connect.getDirectoryVersionSelection parseResult with\n        | Connect.UseDirectoryVersionId selected -> selected |> should equal directoryVersionId\n        | other -> Assert.Fail($\"Unexpected selection: {other}\")\n\n    [<Test>]\n    let ``connect default directory version falls back to based-on`` () =\n        let basedOnId = Guid.NewGuid()\n        let branchDto = { BranchDto.Default with LatestPromotion = ReferenceDto.Default; BasedOn = { ReferenceDto.Default with DirectoryId = basedOnId } }\n\n        Connect.resolveDefaultDirectoryVersionId branchDto\n        |> should equal (Some basedOnId)\n\n    [<Test>]\n    let ``connect repository shortcut populates owner organization repository`` () =\n        let parseResult = GraceCommand.rootCommand.Parse([| \"connect\"; \"owner/org/repo\" |])\n        let graceIds = GraceIds.Default\n\n        match Connect.applyRepositoryShortcut parseResult graceIds with\n        | Ok updated ->\n            updated.OwnerName |> should equal \"owner\"\n            updated.OrganizationName |> should equal \"org\"\n            updated.RepositoryName |> should equal \"repo\"\n            updated.OwnerId |> should equal Guid.Empty\n            updated.OrganizationId |> should equal Guid.Empty\n            updated.RepositoryId |> should equal Guid.Empty\n            updated.HasOwner |> should equal true\n            updated.HasOrganization |> should equal true\n            updated.HasRepository |> should equal true\n        | Error error -> Assert.Fail($\"Unexpected error: {error.Error}\")\n\n    [<Test>]\n    let ``connect repository shortcut rejects missing segments`` () =\n        let parseResult = GraceCommand.rootCommand.Parse([| \"connect\"; \"owner/repo\" |])\n        let graceIds = GraceIds.Default\n\n        match Connect.applyRepositoryShortcut parseResult graceIds with\n        | Ok _ -> Assert.Fail(\"Expected error when repository shortcut is missing segments.\")\n        | Error error ->\n            error.Error\n            |> should contain \"owner/organization/repository\"\n\n    [<Test>]\n    let ``connect repository shortcut conflicts with explicit options`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                [|\n                    \"connect\"\n                    \"owner/org/repo\"\n                    OptionName.OwnerName\n                    \"explicit-owner\"\n                |]\n            )\n\n        let graceIds = GraceIds.Default\n\n        match Connect.applyRepositoryShortcut parseResult graceIds with\n        | Ok _ -> Assert.Fail(\"Expected error when shortcut is combined with explicit options.\")\n        | Error error ->\n            error.Error\n            |> should contain \"Provide either the repository shortcut\"\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<PlatformTarget>x64</PlatformTarget>\n\t\t<Prefer32Bit>false</Prefer32Bit>\n\t\t<LangVersion>preview</LangVersion>\n\t\t<IsPackable>false</IsPackable>\n\t\t<GenerateProgramFile>false</GenerateProgramFile>\n\t\t<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n\t\t<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n\t\t<OtherFlags>--test:GraphBasedChecking</OtherFlags>\n\t\t<OtherFlags>--test:ParallelOptimization</OtherFlags>\n\t\t<OtherFlags>--test:ParallelIlxGen</OtherFlags>\n\t</PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"Agent.CLI.Tests.fs\" />\n    <Compile Include=\"Auth.Tests.fs\" />\n    <Compile Include=\"AuthTokenBundle.Tests.fs\" />\n    <Compile Include=\"Connect.CLI.Tests.fs\" />\n    <Compile Include=\"Program.CLI.Tests.fs\" />\n    <Compile Include=\"HistoryStorage.CLI.Tests.fs\" />\n    <Compile Include=\"LocalStateDb.Tests.fs\" />\n    <Compile Include=\"History.CLI.Tests.fs\" />\n    <Compile Include=\"WorkItem.CLI.Tests.fs\" />\n    <Compile Include=\"Review.CLI.Tests.fs\" />\n    <Compile Include=\"Queue.CLI.Tests.fs\" />\n    <Compile Include=\"PromotionSet.CLI.Tests.fs\" />\n    <Compile Include=\"Watch.Tests.fs\" />\n    <Compile Include=\"Program.fs\" />\n  </ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"FsCheck\" Version=\"3.2.0\" />\n\t\t<PackageReference Include=\"FsCheck.NUnit\" Version=\"3.2.0\" />\n\t\t<PackageReference Include=\"FsUnit\" Version=\"7.1.1\" />\n\t\t<PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n\t\t<PackageReference Include=\"NUnit\" Version=\"4.4.0\" />\n\t\t<PackageReference Include=\"NUnit3TestAdapter\" Version=\"5.2.0\" />\n\t\t<PackageReference Include=\"NUnit.Analyzers\" Version=\"4.11.2\">\n\t\t\t<PrivateAssets>all</PrivateAssets>\n\t\t\t<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t</PackageReference>\n\t\t<PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\">\n\t\t\t<PrivateAssets>all</PrivateAssets>\n\t\t\t<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n\t\t</PackageReference>\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\Grace.CLI\\Grace.CLI.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.CLI.LocalStateDb.Worker\\Grace.CLI.LocalStateDb.Worker.fsproj\" ReferenceOutputAssembly=\"false\" />\n\t\t<ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n\t</ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.CLI.Tests/History.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.CLI.Command\nopen Grace.Shared.Utilities\nopen NodaTime\nopen NUnit.Framework\nopen System\n\n[<TestFixture>]\nmodule HistoryCommandTests =\n    let private createEntry (offsetMinutes: float) (source: string option) (commandLine: string) : HistoryStorage.HistoryEntry =\n        let timestamp = getCurrentInstant ()\n        let offset = Duration.FromMinutes(offsetMinutes)\n\n        {\n            id = Guid.NewGuid()\n            timestampUtc = timestamp.Plus(offset)\n            argvOriginal = [| \"history\"; \"show\" |]\n            argvNormalized = [| \"history\"; \"show\" |]\n            commandLine = commandLine\n            cwd = Environment.CurrentDirectory\n            repoRoot = None\n            repoName = None\n            repoBranch = None\n            graceVersion = \"0.1\"\n            exitCode = 0\n            durationMs = 5L\n            parseSucceeded = true\n            redactions = List.empty\n            source = source\n        }\n\n    [<Test>]\n    let ``filterEntries applies case insensitive source filter`` () =\n        let entries =\n            [\n                createEntry 0.0 (Some \"codex\") \"workitem show\"\n                createEntry 1.0 (Some \"manual\") \"workitem show\"\n                createEntry 2.0 None \"workitem show\"\n            ]\n\n        let filtered = History.filterEntries entries 50 false false false None None (Some \"CODEX\")\n\n        filtered.Length |> should equal 1\n        filtered[0].source |> should equal (Some \"codex\")\n\n    [<Test>]\n    let ``filterEntries combines source and text filters`` () =\n        let entries =\n            [\n                createEntry 0.0 (Some \"codex\") \"branch status\"\n                createEntry 1.0 (Some \"codex\") \"workitem show --id 10\"\n                createEntry 2.0 (Some \"manual\") \"workitem show --id 11\"\n            ]\n\n        let filtered = History.filterEntries entries 50 false false false None (Some \"workitem\") (Some \"codex\")\n\n        filtered.Length |> should equal 1\n        filtered[0].source |> should equal (Some \"codex\")\n\n        filtered[0].commandLine\n        |> should contain \"workitem\"\n"
  },
  {
    "path": "src/Grace.CLI.Tests/HistoryStorage.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.Shared\nopen Grace.Shared.Client\nopen Grace.Shared.Utilities\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.IO\nopen System.Text.Json\n\n[<NonParallelizable>]\nmodule HistoryStorageTests =\n\n    let private withFileBackup (path: string) (action: unit -> unit) =\n        let backupPath = path + \".testbackup\"\n        let hadExisting = File.Exists(path)\n\n        if hadExisting then File.Copy(path, backupPath, true)\n\n        try\n            action ()\n        finally\n            if hadExisting then\n                File.Copy(backupPath, path, true)\n                File.Delete(backupPath)\n            elif File.Exists(path) then\n                File.Delete(path)\n\n    let private randomString (random: Random) =\n        let length = random.Next(8, 32)\n        let chars = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n        let buffer = Array.zeroCreate<char> length\n\n        for i in 0 .. length - 1 do\n            buffer[i] <- chars[random.Next(chars.Length)]\n\n        String(buffer)\n\n    [<Test>]\n    let ``redacts --token values`` () =\n        let config = UserConfiguration.UserConfiguration()\n        let random = Random(1337)\n\n        for _ in 0..49 do\n            let value = randomString random\n            let args = [| \"--token\"; value |]\n            let redacted, _ = HistoryStorage.redactArguments args config.History\n\n            redacted\n            |> Array.exists (fun arg -> arg = value)\n            |> should equal false\n\n    [<Test>]\n    let ``redacts --token=value values`` () =\n        let config = UserConfiguration.UserConfiguration()\n        let random = Random(7331)\n\n        for _ in 0..49 do\n            let value = randomString random\n            let args = [| $\"--token={value}\" |]\n            let redacted, _ = HistoryStorage.redactArguments args config.History\n\n            redacted\n            |> Array.exists (fun arg -> arg.Contains(value, StringComparison.Ordinal))\n            |> should equal false\n\n    [<TestCase(\"30s\", 30.0)>]\n    [<TestCase(\"10m\", 600.0)>]\n    [<TestCase(\"24h\", 86400.0)>]\n    [<TestCase(\"7d\", 604800.0)>]\n    let ``parses valid durations`` (input: string, expectedSeconds: float) =\n        match HistoryStorage.tryParseDuration input with\n        | Ok duration ->\n            duration.TotalSeconds\n            |> should equal expectedSeconds\n        | Error error -> Assert.Fail($\"Expected Ok, got Error: {error}\")\n\n    [<TestCase(\"\")>]\n    [<TestCase(\"10x\")>]\n    [<TestCase(\"abc\")>]\n    [<TestCase(\"10\")>]\n    let ``rejects invalid durations`` (input: string) =\n        match HistoryStorage.tryParseDuration input with\n        | Ok _ -> Assert.Fail(\"Expected Error, got Ok.\")\n        | Error _ -> Assert.Pass()\n\n    [<Test>]\n    let ``readHistoryEntries skips corrupt lines`` () =\n        let historyPath = HistoryStorage.getHistoryFilePath ()\n        let historyDir = Path.GetDirectoryName(historyPath)\n        Directory.CreateDirectory(historyDir) |> ignore\n\n        let options = JsonSerializerOptions(Constants.JsonSerializerOptions)\n        options.WriteIndented <- false\n\n        let entry: HistoryStorage.HistoryEntry =\n            {\n                id = Guid.NewGuid()\n                timestampUtc = getCurrentInstant ()\n                argvOriginal = [| \"branch\"; \"status\" |]\n                argvNormalized = [| \"branch\"; \"status\" |]\n                commandLine = \"branch status\"\n                cwd = Environment.CurrentDirectory\n                repoRoot = None\n                repoName = None\n                repoBranch = None\n                graceVersion = \"0.1\"\n                exitCode = 0\n                durationMs = 10L\n                parseSucceeded = true\n                redactions = List.empty\n                source = None\n            }\n\n        let json = JsonSerializer.Serialize(entry, options)\n\n        withFileBackup historyPath (fun () ->\n            File.WriteAllLines(historyPath, [| json; \"not json\" |])\n            let result = HistoryStorage.readHistoryEntries ()\n            result.Entries.Length |> should equal 1\n            result.CorruptCount |> should equal 1)\n\n    [<Test>]\n    let ``prunes history to max entries`` () =\n        let historyPath = HistoryStorage.getHistoryFilePath ()\n        let configPath = UserConfiguration.getUserConfigurationPath ()\n        let historyDir = Path.GetDirectoryName(historyPath)\n        Directory.CreateDirectory(historyDir) |> ignore\n\n        withFileBackup configPath (fun () ->\n            withFileBackup historyPath (fun () ->\n                let configuration = UserConfiguration.UserConfiguration()\n                configuration.History.Enabled <- true\n                configuration.History.MaxEntries <- 2\n                configuration.History.MaxFileBytes <- 1024L * 1024L\n                configuration.History.RetentionDays <- 365\n\n                match UserConfiguration.saveUserConfiguration configuration with\n                | Ok _ -> ()\n                | Error error -> Assert.Fail(error)\n\n                File.WriteAllText(historyPath, String.Empty)\n\n                let start = getCurrentInstant ()\n\n                for offset in [ 0..2 ] do\n                    let timestamp = start.Plus(Duration.FromMinutes(float offset))\n\n                    HistoryStorage.tryRecordInvocation\n                        {\n                            argvOriginal = [| \"branch\"; \"status\" |]\n                            argvNormalized = [| \"branch\"; \"status\" |]\n                            cwd = Environment.CurrentDirectory\n                            exitCode = 0\n                            durationMs = 5L\n                            parseSucceeded = true\n                            timestampUtc = timestamp\n                            source = None\n                        }\n\n                let result = HistoryStorage.readHistoryEntries ()\n                result.Entries.Length |> should equal 2))\n\n    [<Test>]\n    let ``recordInvocation trims source metadata`` () =\n        let configPath = UserConfiguration.getUserConfigurationPath ()\n\n        withFileBackup configPath (fun () ->\n            let configuration = UserConfiguration.UserConfiguration()\n            configuration.History.Enabled <- true\n            configuration.History.RecordHistoryCommands <- true\n\n            match UserConfiguration.saveUserConfiguration configuration with\n            | Ok _ -> ()\n            | Error error -> Assert.Fail(error)\n\n            let input: HistoryStorage.RecordInput =\n                {\n                    argvOriginal = [| \"branch\"; \"status\" |]\n                    argvNormalized = [| \"branch\"; \"status\" |]\n                    cwd = Environment.CurrentDirectory\n                    exitCode = 0\n                    durationMs = 5L\n                    parseSucceeded = true\n                    timestampUtc = getCurrentInstant ()\n                    source = Some \"  codex-session  \"\n                }\n\n            match HistoryStorage.recordInvocation input with\n            | Some (entry, _) ->\n                entry.source\n                |> should equal (Some \"codex-session\")\n            | None -> Assert.Fail(\"Expected recordInvocation to return a history entry.\"))\n\n    [<Test>]\n    let ``shouldRecord treats history command with leading source option as history`` () =\n        let configuration = UserConfiguration.UserConfiguration()\n        configuration.History.Enabled <- true\n        configuration.History.RecordHistoryCommands <- false\n\n        let input: HistoryStorage.RecordInput =\n            {\n                argvOriginal =\n                    [|\n                        \"--source\"\n                        \"codex\"\n                        \"history\"\n                        \"show\"\n                    |]\n                argvNormalized =\n                    [|\n                        \"--source\"\n                        \"codex\"\n                        \"history\"\n                        \"show\"\n                    |]\n                cwd = Environment.CurrentDirectory\n                exitCode = 0\n                durationMs = 5L\n                parseSucceeded = true\n                timestampUtc = getCurrentInstant ()\n                source = Some \"codex\"\n            }\n\n        HistoryStorage.shouldRecord input configuration.History\n        |> should equal false\n"
  },
  {
    "path": "src/Grace.CLI.Tests/LocalStateDb.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Microsoft.Data.Sqlite\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\nopen System.IO\nopen System.Linq\nopen System.Text\nopen System.Diagnostics\nopen System.Threading.Tasks\n\n[<NonParallelizable>]\nmodule LocalStateDbTests =\n    let private configureVerboseLogging () =\n        let value = Environment.GetEnvironmentVariable(\"GRACE_LOCALSTATE_DB_VERBOSE\")\n\n        if not (String.IsNullOrWhiteSpace(value)) then\n            let enabled =\n                value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n                || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase)\n\n            LocalStateDb.setVerbose enabled\n\n    let private configureForRoot (root: string) =\n        let configuration = GraceConfiguration()\n        configuration.OwnerId <- Guid.NewGuid()\n        configuration.OrganizationId <- Guid.NewGuid()\n        configuration.RepositoryId <- Guid.NewGuid()\n        configuration.RootDirectory <- root\n        configuration.StandardizedRootDirectory <- normalizeFilePath root\n        configuration.GraceDirectory <- Path.Combine(root, Constants.GraceConfigDirectory)\n        configuration.ObjectDirectory <- Path.Combine(configuration.GraceDirectory, Constants.GraceObjectsDirectory)\n        configuration.GraceStatusFile <- Path.Combine(configuration.GraceDirectory, Constants.GraceLocalStateDbFileName)\n        configuration.GraceObjectCacheFile <- configuration.GraceStatusFile\n        configuration.ConfigurationDirectory <- configuration.GraceDirectory\n        configuration.IsPopulated <- true\n        updateConfiguration configuration\n        configuration\n\n    let private ensureGraceConfig (root: string) =\n        let graceDir = Path.Combine(root, Constants.GraceConfigDirectory)\n        let configPath = Path.Combine(graceDir, Constants.GraceConfigFileName)\n\n        if not (Directory.Exists(graceDir)) then\n            Directory.CreateDirectory(graceDir) |> ignore\n\n        if not (File.Exists(configPath)) then\n            File.WriteAllText(configPath, \"{}\")\n\n    let private withTempDir (action: string -> GraceConfiguration -> Task<'T>) =\n        task {\n            let root = Path.Combine(Path.GetTempPath(), $\"grace-tests-{Guid.NewGuid()}\")\n            Directory.CreateDirectory(root) |> ignore\n            let previousDirectory = Environment.CurrentDirectory\n            let previousConfiguration =\n                if configurationFileExists () then\n                    Some(Current())\n                else\n                    None\n\n            try\n                Environment.CurrentDirectory <- root\n                configureVerboseLogging ()\n                ensureGraceConfig root\n                let configuration = configureForRoot root\n                return! action root configuration\n            finally\n                Environment.CurrentDirectory <- previousDirectory\n\n                match previousConfiguration with\n                | Some configuration -> updateConfiguration configuration\n                | None -> resetConfiguration ()\n\n                if Directory.Exists(root) then\n                    try\n                        SqliteConnection.ClearAllPools()\n                        Directory.Delete(root, true)\n                    with\n                    | _ -> ()\n        }\n\n    let private createFileVersion relativePath sha256Hash isBinary size createdAt lastWriteTime =\n        LocalFileVersion.Create relativePath sha256Hash isBinary size createdAt true lastWriteTime\n\n    let private createDirectoryVersion\n        (configuration: GraceConfiguration)\n        (directoryVersionId: DirectoryVersionId)\n        relativePath\n        sha256Hash\n        (directoryIds: DirectoryVersionId array)\n        (files: LocalFileVersion array)\n        sizeBytes\n        lastWriteTimeUtc\n        =\n        LocalDirectoryVersion.Create\n            directoryVersionId\n            configuration.OwnerId\n            configuration.OrganizationId\n            configuration.RepositoryId\n            relativePath\n            sha256Hash\n            (List<DirectoryVersionId>(directoryIds))\n            (List<LocalFileVersion>(files))\n            sizeBytes\n            lastWriteTimeUtc\n\n    let private openRawConnection (dbPath: string) =\n        let connection = new SqliteConnection($\"Data Source={dbPath}\")\n        connection.Open()\n        connection\n\n    let private executeScalarString (connection: SqliteConnection) (sql: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        cmd.ExecuteScalar() :?> string\n\n    let private executeScalarInt (connection: SqliteConnection) (sql: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        cmd.ExecuteScalar() |> Convert.ToInt32\n\n    let private executeScalarInt64 (connection: SqliteConnection) (sql: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        cmd.ExecuteScalar() |> Convert.ToInt64\n\n    let private executeNonQuery (connection: SqliteConnection) (sql: string) =\n        use cmd = connection.CreateCommand()\n        cmd.CommandText <- sql\n        cmd.ExecuteNonQuery() |> ignore\n\n    let private getCorruptBackups (dbPath: string) =\n        let directoryPath = Path.GetDirectoryName(dbPath)\n\n        if String.IsNullOrWhiteSpace(directoryPath) then\n            Array.Empty<string>()\n        else\n            Directory.GetFiles(directoryPath, \"grace-local.corrupt.*.db\")\n\n    let private seedSchemaVersionOnly (dbPath: string) (schemaVersion: string) =\n        Directory.CreateDirectory(Path.GetDirectoryName(dbPath))\n        |> ignore\n\n        use connection = openRawConnection dbPath\n        executeNonQuery connection \"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\"\n        executeNonQuery connection $\"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '{schemaVersion}');\"\n\n    let private createTestStatus (rootId: Guid) (rootHash: string) (ticks: int64) =\n        { GraceStatus.Default with\n            RootDirectoryId = rootId\n            RootDirectorySha256Hash = rootHash\n            LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(ticks)\n            LastSuccessfulDirectoryVersionUpload = Instant.FromUnixTimeTicks(ticks)\n        }\n\n    type private WorkerCommand = { FileName: string; ArgumentsPrefix: string }\n\n    let private tryGetWorkerCommand () =\n        try\n            let baseDir = AppContext.BaseDirectory\n            let tfm = DirectoryInfo(baseDir).Name\n            let config = DirectoryInfo(baseDir).Parent.Name\n\n            let mutable current = DirectoryInfo(baseDir)\n            let mutable srcDir = Unchecked.defaultof<DirectoryInfo>\n            let mutable found = false\n\n            while (not found) && (not <| isNull current) do\n                if current.Name.Equals(\"src\", StringComparison.OrdinalIgnoreCase) then\n                    srcDir <- current\n                    found <- true\n                else\n                    current <- current.Parent\n\n            if not found then\n                None\n            else\n                let workerBinDir = Path.Combine(srcDir.FullName, \"Grace.CLI.LocalStateDb.Worker\", \"bin\", config, tfm)\n\n                let exePath = Path.Combine(workerBinDir, \"Grace.CLI.LocalStateDb.Worker.exe\")\n                let dllPath = Path.Combine(workerBinDir, \"Grace.CLI.LocalStateDb.Worker.dll\")\n\n                if File.Exists(exePath) then\n                    Some { FileName = exePath; ArgumentsPrefix = String.Empty }\n                elif File.Exists(dllPath) then\n                    Some { FileName = \"dotnet\"; ArgumentsPrefix = $\"\\\"{dllPath}\\\"\" }\n                else\n                    None\n        with\n        | _ -> None\n\n    [<Test>]\n    let ``initializes schema and status meta`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = new SqliteConnection($\"Data Source={configuration.GraceStatusFile}\")\n                connection.Open()\n\n                use cmd = connection.CreateCommand()\n                cmd.CommandText <- \"SELECT value FROM meta WHERE key = 'schema_version';\"\n                let schemaVersion = cmd.ExecuteScalar() :?> string\n                schemaVersion |> should equal \"2\"\n\n                cmd.CommandText <- \"SELECT COUNT(*) FROM status_meta;\"\n                let statusMetaCount = Convert.ToInt32(cmd.ExecuteScalar())\n                statusMetaCount |> should equal 1\n            })\n\n    [<Test>]\n    let ``round trips status snapshot`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = getCurrentInstant ()\n                let lastWrite = DateTime.UtcNow\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n\n                let rootFile = createFileVersion \"root.txt\" \"root-hash\" false 12L now lastWrite\n                let srcFile = createFileVersion \"src/file.txt\" \"src-hash\" false 34L now lastWrite\n\n                let srcDirectory = createDirectoryVersion configuration srcId \"src\" \"src-dir-hash\" [||] [| srcFile |] srcFile.Size lastWrite\n\n                let rootDirectory =\n                    createDirectoryVersion\n                        configuration\n                        rootId\n                        Constants.RootDirectoryPath\n                        \"root-dir-hash\"\n                        [| srcId |]\n                        [| rootFile |]\n                        (rootFile.Size + srcDirectory.Size)\n                        lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDirectory) |> ignore\n                index.TryAdd(srcId, srcDirectory) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDirectory.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                readBack.RootDirectoryId |> should equal rootId\n\n                readBack.RootDirectorySha256Hash\n                |> should equal rootDirectory.Sha256Hash\n\n                readBack.Index.Count |> should equal 2\n\n                let files =\n                    readBack.Index.Values\n                    |> Seq.collect (fun dv -> dv.Files)\n                    |> Seq.toList\n\n                files.Length |> should equal 2\n\n                let srcRead =\n                    files\n                    |> Seq.find (fun file -> file.RelativePath = \"src/file.txt\")\n\n                srcRead.Sha256Hash |> should equal \"src-hash\"\n                srcRead.Size |> should equal 34L\n            })\n\n    [<Test>]\n    let ``applies incremental updates`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = getCurrentInstant ()\n                let lastWrite = DateTime.UtcNow\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n                let oldId = Guid.NewGuid()\n\n                let rootFile = createFileVersion \"root.txt\" \"root-hash\" false 10L now lastWrite\n                let srcFile = createFileVersion \"src/file.txt\" \"src-hash\" false 20L now lastWrite\n\n                let oldDirectory = createDirectoryVersion configuration oldId \"old\" \"old-dir-hash\" [||] [||] 0L lastWrite\n\n                let srcDirectory = createDirectoryVersion configuration srcId \"src\" \"src-dir-hash\" [||] [| srcFile |] srcFile.Size lastWrite\n\n                let rootDirectory =\n                    createDirectoryVersion\n                        configuration\n                        rootId\n                        Constants.RootDirectoryPath\n                        \"root-dir-hash\"\n                        [| srcId; oldId |]\n                        [| rootFile |]\n                        (rootFile.Size + srcDirectory.Size)\n                        lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDirectory) |> ignore\n                index.TryAdd(srcId, srcDirectory) |> ignore\n                index.TryAdd(oldId, oldDirectory) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDirectory.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n\n                let newRootId = Guid.NewGuid()\n                let newSrcId = Guid.NewGuid()\n                let changedFile = createFileVersion \"src/file.txt\" \"src-hash-2\" false 25L now lastWrite\n                let newFile = createFileVersion \"src/new.txt\" \"new-hash\" false 5L now lastWrite\n\n                let newSrcDirectory =\n                    createDirectoryVersion\n                        configuration\n                        newSrcId\n                        \"src\"\n                        \"src-dir-hash-2\"\n                        [||]\n                        [| changedFile; newFile |]\n                        (changedFile.Size + newFile.Size)\n                        lastWrite\n\n                let newRootDirectory =\n                    createDirectoryVersion\n                        configuration\n                        newRootId\n                        Constants.RootDirectoryPath\n                        \"root-dir-hash-2\"\n                        [| newSrcId |]\n                        [||]\n                        (changedFile.Size + newFile.Size)\n                        lastWrite\n\n                let newIndex = GraceIndex()\n\n                newIndex.TryAdd(newRootId, newRootDirectory)\n                |> ignore\n\n                newIndex.TryAdd(newSrcId, newSrcDirectory)\n                |> ignore\n\n                let updatedStatus = { status with Index = newIndex; RootDirectoryId = newRootId; RootDirectorySha256Hash = newRootDirectory.Sha256Hash }\n\n                let differences =\n                    [\n                        FileSystemDifference.Create Change FileSystemEntryType.File \"src/file.txt\"\n                        FileSystemDifference.Create Add FileSystemEntryType.File \"src/new.txt\"\n                        FileSystemDifference.Create Delete FileSystemEntryType.File \"root.txt\"\n                        FileSystemDifference.Create Delete FileSystemEntryType.Directory \"old\"\n                    ]\n\n                do! LocalStateDb.applyStatusIncremental configuration.GraceStatusFile updatedStatus [ newSrcDirectory; newRootDirectory ] differences\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n                readBack.RootDirectoryId |> should equal newRootId\n\n                readBack.Index.ContainsKey(oldId)\n                |> should equal false\n\n                let srcRead =\n                    readBack.Index.Values\n                    |> Seq.find (fun dv -> dv.RelativePath = \"src\")\n\n                srcRead.Files.Count |> should equal 2\n\n                srcRead.Files\n                |> Seq.exists (fun file -> file.RelativePath = \"src/new.txt\")\n                |> should equal true\n\n                readBack.Index.Values\n                |> Seq.collect (fun dv -> dv.Files)\n                |> Seq.exists (fun file -> file.RelativePath = \"root.txt\")\n                |> should equal false\n            })\n\n    [<Test>]\n    let ``upserts object cache entries`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = getCurrentInstant ()\n                let lastWrite = DateTime.UtcNow\n                let directoryId = Guid.NewGuid()\n                let fileVersion = createFileVersion \"src/cache.txt\" \"cache-hash\" false 12L now lastWrite\n\n                let directory = createDirectoryVersion configuration directoryId \"src\" \"cache-dir-hash\" [||] [| fileVersion |] fileVersion.Size lastWrite\n\n                do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ directory ]\n\n                let! directoryExists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile directoryId\n\n                directoryExists |> should equal true\n\n                let! fileExists = LocalStateDb.isFileVersionInObjectCache configuration.GraceStatusFile fileVersion\n\n                fileExists |> should equal true\n            })\n\n    [<Test>]\n    let ``concurrent writers do not corrupt database`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let rootId = Guid.NewGuid()\n                let rootHash = \"root-hash\"\n                let baseTicks = getCurrentInstant().ToUnixTimeTicks()\n\n                let tasks =\n                    Array.init 8 (fun index ->\n                        task {\n                            let status =\n                                { GraceStatus.Default with\n                                    RootDirectoryId = rootId\n                                    RootDirectorySha256Hash = rootHash\n                                    LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(baseTicks + int64 index)\n                                }\n\n                            do! LocalStateDb.applyStatusIncremental configuration.GraceStatusFile status Seq.empty Seq.empty\n                        })\n\n                do! Task.WhenAll(tasks |> Array.map (fun task -> task :> Task))\n\n                let! meta = LocalStateDb.readStatusMeta configuration.GraceStatusFile\n                meta.RootDirectoryId |> should equal rootId\n\n                meta.RootDirectorySha256Hash\n                |> should equal rootHash\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized recreates DB when schema_version mismatches`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                seedSchemaVersionOnly configuration.GraceStatusFile \"0\"\n\n                let corruptBefore =\n                    getCorruptBackups configuration.GraceStatusFile\n                    |> Array.length\n\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let schemaVersion = executeScalarString connection \"SELECT value FROM meta WHERE key = 'schema_version';\"\n                schemaVersion |> should equal \"2\"\n\n                let corruptAfter =\n                    getCorruptBackups configuration.GraceStatusFile\n                    |> Array.length\n\n                corruptAfter |> should equal (corruptBefore + 1)\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized recovers from a corrupt non-sqlite file`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                Directory.CreateDirectory(Path.GetDirectoryName(configuration.GraceStatusFile))\n                |> ignore\n\n                let corruptBefore =\n                    getCorruptBackups configuration.GraceStatusFile\n                    |> Array.length\n\n                let bytes = Encoding.UTF8.GetBytes(\"this is not a sqlite database\")\n                File.WriteAllBytes(configuration.GraceStatusFile, bytes)\n\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let schemaVersion = executeScalarString connection \"SELECT value FROM meta WHERE key = 'schema_version';\"\n                schemaVersion |> should equal \"2\"\n\n                let corruptAfter =\n                    getCorruptBackups configuration.GraceStatusFile\n                    |> Array.length\n\n                corruptAfter |> should equal (corruptBefore + 1)\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized recreation refreshes sidecar files`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                seedSchemaVersionOnly configuration.GraceStatusFile \"0\"\n                let oldTime = DateTime.UtcNow.AddDays(-1)\n\n                let journalPath = configuration.GraceStatusFile + \"-journal\"\n                let walPath = configuration.GraceStatusFile + \"-wal\"\n                let shmPath = configuration.GraceStatusFile + \"-shm\"\n\n                File.WriteAllText(journalPath, \"sentinel\")\n                File.WriteAllText(walPath, \"sentinel\")\n                File.WriteAllText(shmPath, \"sentinel\")\n\n                File.SetLastWriteTimeUtc(journalPath, oldTime)\n                File.SetLastWriteTimeUtc(walPath, oldTime)\n                File.SetLastWriteTimeUtc(shmPath, oldTime)\n\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                File.Exists(journalPath) |> should equal false\n\n                if File.Exists(walPath) then\n                    File.GetLastWriteTimeUtc(walPath)\n                    |> should be (greaterThan oldTime)\n\n                if File.Exists(shmPath) then\n                    File.GetLastWriteTimeUtc(shmPath)\n                    |> should be (greaterThan oldTime)\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized creates expected tables and indexes`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                use cmd = connection.CreateCommand()\n                cmd.CommandText <- \"SELECT type, name FROM sqlite_master WHERE type IN ('table', 'index');\"\n                use reader = cmd.ExecuteReader()\n\n                let objects = HashSet<string>(StringComparer.OrdinalIgnoreCase)\n\n                while reader.Read() do\n                    let objectType = reader.GetString(0)\n                    let name = reader.GetString(1)\n                    objects.Add($\"{objectType}:{name}\") |> ignore\n\n                let expected =\n                    [|\n                        \"table:meta\"\n                        \"table:status_meta\"\n                        \"table:status_directories\"\n                        \"index:ix_status_directories_directory_version_id\"\n                        \"table:status_files\"\n                        \"index:ix_status_files_directory_path\"\n                        \"index:ix_status_files_directory_version_id\"\n                        \"index:ix_status_files_sha256\"\n                        \"table:object_cache_directories\"\n                        \"index:ix_object_cache_directories_relative_path\"\n                        \"table:object_cache_directory_children\"\n                        \"index:ix_object_cache_children_parent\"\n                        \"table:object_cache_directory_files\"\n                        \"index:ix_object_cache_files_path_hash\"\n                    |]\n\n                expected\n                |> Array.iter (fun name -> objects.Contains(name) |> should equal true)\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized is idempotent and preserves created_at`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection1 = openRawConnection configuration.GraceStatusFile\n                let createdAt1 = executeScalarInt64 connection1 \"SELECT CAST(value AS INTEGER) FROM meta WHERE key = 'created_at_unix_ticks';\"\n                let statusMetaCount1 = executeScalarInt connection1 \"SELECT COUNT(*) FROM status_meta;\"\n                statusMetaCount1 |> should equal 1\n\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection2 = openRawConnection configuration.GraceStatusFile\n                let createdAt2 = executeScalarInt64 connection2 \"SELECT CAST(value AS INTEGER) FROM meta WHERE key = 'created_at_unix_ticks';\"\n                createdAt2 |> should equal createdAt1\n\n                let statusMetaCount2 = executeScalarInt connection2 \"SELECT COUNT(*) FROM status_meta;\"\n                statusMetaCount2 |> should equal 1\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized does not overwrite existing status_meta row`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                Directory.CreateDirectory(Path.GetDirectoryName(configuration.GraceStatusFile))\n                |> ignore\n\n                let rootId = Guid.NewGuid()\n                let rootHash = \"custom-root-hash\"\n                let ticks = 1234567890L\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                executeNonQuery connection \"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\"\n\n                executeNonQuery\n                    connection\n                    \"CREATE TABLE IF NOT EXISTS status_meta (id INTEGER PRIMARY KEY CHECK (id = 1), root_directory_version_id TEXT NOT NULL, root_directory_sha256_hash TEXT NOT NULL, last_successful_file_upload_unix_ticks INTEGER NOT NULL, last_successful_directory_version_upload_unix_ticks INTEGER NOT NULL);\"\n\n                executeNonQuery connection \"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '2');\"\n\n                executeNonQuery\n                    connection\n                    $\"INSERT OR REPLACE INTO status_meta (id, root_directory_version_id, root_directory_sha256_hash, last_successful_file_upload_unix_ticks, last_successful_directory_version_upload_unix_ticks) VALUES (1, '{rootId}', '{rootHash}', {ticks}, {ticks});\"\n\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection2 = openRawConnection configuration.GraceStatusFile\n                let readRootId = executeScalarString connection2 \"SELECT root_directory_version_id FROM status_meta WHERE id = 1;\"\n                let readRootHash = executeScalarString connection2 \"SELECT root_directory_sha256_hash FROM status_meta WHERE id = 1;\"\n                readRootId |> should equal (rootId.ToString())\n                readRootHash |> should equal rootHash\n            })\n\n    [<Test>]\n    let ``journal mode is WAL after initialization`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let journalMode = executeScalarString connection \"PRAGMA journal_mode;\"\n\n                journalMode.ToLowerInvariant()\n                |> should equal \"wal\"\n            })\n\n    [<Test>]\n    let ``busy writer retries and eventually succeeds`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use lockConnection = openRawConnection configuration.GraceStatusFile\n                executeNonQuery lockConnection \"BEGIN IMMEDIATE;\"\n\n                let rootId = Guid.NewGuid()\n                let rootHash = \"root-hash\"\n                let ticks = getCurrentInstant().ToUnixTimeTicks()\n                let status = createTestStatus rootId rootHash ticks\n\n                let writerTask = task { do! LocalStateDb.applyStatusIncremental configuration.GraceStatusFile status Seq.empty Seq.empty }\n\n                do! Task.Delay(450)\n                executeNonQuery lockConnection \"COMMIT;\"\n\n                do! writerTask\n\n                let! meta = LocalStateDb.readStatusMeta configuration.GraceStatusFile\n                meta.RootDirectoryId |> should equal rootId\n\n                meta.RootDirectorySha256Hash\n                |> should equal rootHash\n            })\n\n    [<Test>]\n    let ``non-busy sqlite failures are not retried`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                executeNonQuery connection \"CREATE TRIGGER abort_status_meta BEFORE INSERT ON status_meta BEGIN SELECT RAISE(ABORT,'boom'); END;\"\n\n                let stopwatch = Stopwatch.StartNew()\n\n                let rootId = Guid.NewGuid()\n                let rootHash = \"root-hash\"\n                let ticks = getCurrentInstant().ToUnixTimeTicks()\n                let status = createTestStatus rootId rootHash ticks\n\n                let operation = fun () -> task { do! LocalStateDb.applyStatusIncremental configuration.GraceStatusFile status Seq.empty Seq.empty } :> Task\n\n                Assert.ThrowsAsync<SqliteException>(operation)\n                |> ignore\n\n                stopwatch.Stop()\n\n                stopwatch.ElapsedMilliseconds\n                |> should be (lessThan 1500L)\n            })\n\n    [<Test>]\n    let ``replaceStatusSnapshot is atomic (rollback on failure)`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(111L)\n                let lastWrite = DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n\n                let rootDirectory = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-dir-hash\" [||] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDirectory) |> ignore\n\n                let statusA =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDirectory.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile statusA\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                executeNonQuery connection \"CREATE TRIGGER abort_status_files BEFORE INSERT ON status_files BEGIN SELECT RAISE(ABORT,'boom'); END;\"\n\n                let rootFile = createFileVersion \"root.txt\" \"root-file-hash-NEW\" false 1L now lastWrite\n\n                let rootDirectoryB =\n                    createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-dir-hash-NEW\" [||] [| rootFile |] rootFile.Size lastWrite\n\n                let indexB = GraceIndex()\n                indexB.TryAdd(rootId, rootDirectoryB) |> ignore\n\n                let statusB =\n                    { statusA with\n                        Index = indexB\n                        RootDirectorySha256Hash = rootDirectoryB.Sha256Hash\n                        LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(999L)\n                    }\n\n                let operation = fun () -> task { do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile statusB } :> Task\n\n                Assert.ThrowsAsync<SqliteException>(operation)\n                |> ignore\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                readBack.RootDirectorySha256Hash\n                |> should equal statusA.RootDirectorySha256Hash\n\n                readBack.LastSuccessfulFileUpload\n                |> should equal statusA.LastSuccessfulFileUpload\n\n                readBack.Index.Count |> should equal 1\n            })\n\n    [<Test>]\n    let ``applyStatusIncremental is atomic (rollback on failure)`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(1000L)\n                let lastWrite = DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n\n                let rootDirectory = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-dir-hash\" [| srcId |] [||] 0L lastWrite\n\n                let srcDirectory = createDirectoryVersion configuration srcId \"src\" \"src-dir-hash\" [||] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDirectory) |> ignore\n                index.TryAdd(srcId, srcDirectory) |> ignore\n\n                let statusA =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDirectory.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile statusA\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                executeNonQuery connection \"CREATE TRIGGER abort_status_files BEFORE INSERT ON status_files BEGIN SELECT RAISE(ABORT,'boom'); END;\"\n\n                let newFile = createFileVersion \"src/file.txt\" \"hash-1\" false 11L now lastWrite\n\n                let updatedSrc = createDirectoryVersion configuration srcId \"src\" \"src-dir-hash\" [||] [| newFile |] newFile.Size lastWrite\n\n                let updatedStatus = { statusA with RootDirectorySha256Hash = \"root-dir-hash-NEW\"; LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(2222L) }\n\n                let differences =\n                    [\n                        FileSystemDifference.Create Add FileSystemEntryType.File \"src/file.txt\"\n                    ]\n\n                let operation =\n                    fun () -> task { do! LocalStateDb.applyStatusIncremental configuration.GraceStatusFile updatedStatus [ updatedSrc ] differences } :> Task\n\n                Assert.ThrowsAsync<SqliteException>(operation)\n                |> ignore\n\n                let! meta = LocalStateDb.readStatusMeta configuration.GraceStatusFile\n\n                meta.RootDirectorySha256Hash\n                |> should equal statusA.RootDirectorySha256Hash\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                readBack.Index.Values\n                |> Seq.collect (fun dv -> dv.Files)\n                |> Seq.exists (fun file -> file.RelativePath = \"src/file.txt\")\n                |> should equal false\n            })\n\n    [<Test>]\n    let ``concurrent ensureDbInitialized calls do not deadlock or corrupt`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let tasks = Array.init 16 (fun _ -> LocalStateDb.ensureDbInitialized configuration.GraceStatusFile)\n\n                do!\n                    Task\n                        .WhenAll(tasks |> Array.map (fun t -> t :> Task))\n                        .WaitAsync(TimeSpan.FromSeconds(15.0))\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let schemaVersion = executeScalarString connection \"SELECT value FROM meta WHERE key = 'schema_version';\"\n                schemaVersion |> should equal \"2\"\n\n                let statusMetaCount = executeScalarInt connection \"SELECT COUNT(*) FROM status_meta;\"\n                statusMetaCount |> should equal 1\n            })\n\n    [<Test>]\n    let ``ensureDbInitialized treats paths case-insensitively on Windows`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let pathA = configuration.GraceStatusFile.ToLowerInvariant()\n                let pathB = configuration.GraceStatusFile.ToUpperInvariant()\n\n                let tasks =\n                    [|\n                        LocalStateDb.ensureDbInitialized pathA\n                        LocalStateDb.ensureDbInitialized pathB\n                    |]\n\n                do!\n                    Task\n                        .WhenAll(tasks |> Array.map (fun t -> t :> Task))\n                        .WaitAsync(TimeSpan.FromSeconds(15.0))\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let schemaVersion = executeScalarString connection \"SELECT value FROM meta WHERE key = 'schema_version';\"\n                schemaVersion |> should equal \"2\"\n            })\n\n    [<Test>]\n    let ``replaceStatusSnapshot fully clears old snapshot rows`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(123L)\n                let lastWrite = DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n\n                let rootId1 = Guid.NewGuid()\n                let srcId1 = Guid.NewGuid()\n                let utilId1 = Guid.NewGuid()\n\n                let file1 = createFileVersion \"src/a.txt\" \"a\" false 1L now lastWrite\n                let file2 = createFileVersion \"src/b.txt\" \"b\" false 2L now lastWrite\n                let file3 = createFileVersion \"src/utils/c.txt\" \"c\" false 3L now lastWrite\n\n                let utilDir1 = createDirectoryVersion configuration utilId1 \"src/utils\" \"util-hash\" [||] [| file3 |] file3.Size lastWrite\n\n                let srcDir1 =\n                    createDirectoryVersion\n                        configuration\n                        srcId1\n                        \"src\"\n                        \"src-hash\"\n                        [| utilId1 |]\n                        [| file1; file2 |]\n                        (file1.Size + file2.Size + utilDir1.Size)\n                        lastWrite\n\n                let rootDir1 = createDirectoryVersion configuration rootId1 Constants.RootDirectoryPath \"root-hash\" [| srcId1 |] [||] srcDir1.Size lastWrite\n\n                let index1 = GraceIndex()\n                index1.TryAdd(rootId1, rootDir1) |> ignore\n                index1.TryAdd(srcId1, srcDir1) |> ignore\n                index1.TryAdd(utilId1, utilDir1) |> ignore\n\n                let status1 =\n                    { GraceStatus.Default with\n                        Index = index1\n                        RootDirectoryId = rootId1\n                        RootDirectorySha256Hash = rootDir1.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status1\n\n                let rootId2 = Guid.NewGuid()\n\n                let rootDir2 = createDirectoryVersion configuration rootId2 Constants.RootDirectoryPath \"root-hash-2\" [||] [||] 0L lastWrite\n\n                let index2 = GraceIndex()\n                index2.TryAdd(rootId2, rootDir2) |> ignore\n\n                let status2 = { status1 with Index = index2; RootDirectoryId = rootId2; RootDirectorySha256Hash = rootDir2.Sha256Hash }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status2\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let directoryCount = executeScalarInt connection \"SELECT COUNT(*) FROM status_directories;\"\n                let fileCount = executeScalarInt connection \"SELECT COUNT(*) FROM status_files;\"\n                directoryCount |> should equal 1\n                fileCount |> should equal 0\n            })\n\n    [<Test>]\n    let ``replaceStatusSnapshot writes correct parent_path values`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(456L)\n                let lastWrite = DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n                let utilId = Guid.NewGuid()\n\n                let utilDir = createDirectoryVersion configuration utilId \"src/utils\" \"util-hash\" [||] [||] 0L lastWrite\n\n                let srcDir = createDirectoryVersion configuration srcId \"src\" \"src-hash\" [| utilId |] [||] 0L lastWrite\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [| srcId |] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDir) |> ignore\n                index.TryAdd(srcId, srcDir) |> ignore\n                index.TryAdd(utilId, utilDir) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                let rootParent = executeScalarString connection \"SELECT parent_path FROM status_directories WHERE relative_path = '.';\"\n                let srcParent = executeScalarString connection \"SELECT parent_path FROM status_directories WHERE relative_path = 'src';\"\n                let utilParent = executeScalarString connection \"SELECT parent_path FROM status_directories WHERE relative_path = 'src/utils';\"\n\n                rootParent |> should equal String.Empty\n\n                srcParent\n                |> should equal Constants.RootDirectoryPath\n\n                utilParent |> should equal \"src\"\n            })\n\n    [<Test>]\n    let ``readStatusSnapshot reconstructs child relationships`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(789L)\n                let lastWrite = DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n                let utilId = Guid.NewGuid()\n\n                let utilDir = createDirectoryVersion configuration utilId \"src/utils\" \"util-hash\" [||] [||] 0L lastWrite\n\n                let srcDir = createDirectoryVersion configuration srcId \"src\" \"src-hash\" [| utilId |] [||] 0L lastWrite\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [| srcId |] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDir) |> ignore\n                index.TryAdd(srcId, srcDir) |> ignore\n                index.TryAdd(utilId, utilDir) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                let rootRead = readBack.Index[rootId]\n\n                rootRead.Directories.Contains(srcId)\n                |> should equal true\n\n                let srcRead = readBack.Index[srcId]\n\n                srcRead.Directories.Contains(utilId)\n                |> should equal true\n            })\n\n    [<Test>]\n    let ``readStatusSnapshot round-trips last write ticks as UTC`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(999L)\n                let lastWrite = DateTime(2021, 10, 11, 12, 13, 14, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n\n                let file = createFileVersion \"root.txt\" \"hash\" false 10L now lastWrite\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [||] [| file |] file.Size lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDir) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                let rootRead = readBack.Index[rootId]\n\n                rootRead.LastWriteTimeUtc.Ticks\n                |> should equal lastWrite.Ticks\n\n                rootRead.LastWriteTimeUtc.Kind\n                |> should equal DateTimeKind.Utc\n\n                let fileRead =\n                    rootRead.Files\n                    |> Seq.find (fun f -> f.RelativePath = \"root.txt\")\n\n                fileRead.LastWriteTimeUtc.Ticks\n                |> should equal lastWrite.Ticks\n\n                fileRead.LastWriteTimeUtc.Kind\n                |> should equal DateTimeKind.Utc\n            })\n\n    [<Test>]\n    let ``readStatusSnapshot tolerates missing status_meta row`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(1234L)\n                let lastWrite = DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [||] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDir) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                executeNonQuery connection \"DELETE FROM status_meta;\"\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                readBack.RootDirectoryId\n                |> should equal DirectoryVersionId.Empty\n\n                readBack.RootDirectorySha256Hash\n                |> should equal (Sha256Hash String.Empty)\n\n                readBack.Index.Count |> should equal 1\n            })\n\n    [<Test>]\n    let ``status_files enforces directory_version_id`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                do! LocalStateDb.ensureDbInitialized configuration.GraceStatusFile\n\n                use connection = openRawConnection configuration.GraceStatusFile\n                executeNonQuery connection \"PRAGMA foreign_keys = ON;\"\n\n                executeNonQuery\n                    connection\n                    \"INSERT OR REPLACE INTO status_directories (relative_path, parent_path, directory_version_id, sha256_hash, size_bytes, created_at_unix_ticks, last_write_time_utc_ticks) VALUES ('.', '', '00000000-0000-0000-0000-000000000001', 'root', 0, 0, 0);\"\n\n                let orphanId = Guid.NewGuid()\n\n                Assert.Throws<SqliteException>(\n                    TestDelegate(fun () ->\n                        executeNonQuery\n                            connection\n                            $\"INSERT OR REPLACE INTO status_files (relative_path, directory_path, directory_version_id, sha256_hash, is_binary, size_bytes, created_at_unix_ticks, uploaded_to_object_storage, last_write_time_utc_ticks) VALUES ('orphan.txt', 'missing', '{orphanId}', 'hash', 0, 1, 0, 0, 0);\")\n                )\n                |> ignore\n            })\n\n    [<Test>]\n    let ``applyStatusIncremental upserts add and change file values`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(4000L)\n                let lastWrite1 = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let lastWrite2 = DateTime(2022, 2, 3, 4, 5, 6, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [| srcId |] [||] 0L lastWrite1\n\n                let file1 = LocalFileVersion.Create \"src/file.txt\" \"hash-1\" true 10L now false lastWrite1\n\n                let srcDir1 = createDirectoryVersion configuration srcId \"src\" \"src-hash\" [||] [| file1 |] file1.Size lastWrite1\n\n                let status1 =\n                    { GraceStatus.Default with\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do!\n                    LocalStateDb.applyStatusIncremental\n                        configuration.GraceStatusFile\n                        status1\n                        [ rootDir; srcDir1 ]\n                        [\n                            FileSystemDifference.Create Add FileSystemEntryType.File \"src/file.txt\"\n                        ]\n\n                let! readBack1 = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                let srcRead1 =\n                    readBack1.Index.Values\n                    |> Seq.find (fun dv -> dv.RelativePath = \"src\")\n\n                let fileRead1 =\n                    srcRead1.Files\n                    |> Seq.find (fun f -> f.RelativePath = \"src/file.txt\")\n\n                fileRead1.Sha256Hash |> should equal \"hash-1\"\n                fileRead1.IsBinary |> should equal true\n\n                fileRead1.UploadedToObjectStorage\n                |> should equal false\n\n                fileRead1.Size |> should equal 10L\n\n                fileRead1.LastWriteTimeUtc.Ticks\n                |> should equal lastWrite1.Ticks\n\n                let file2 = LocalFileVersion.Create \"src/file.txt\" \"hash-2\" false 25L now true lastWrite2\n\n                let srcDir2 = createDirectoryVersion configuration srcId \"src\" \"src-hash-2\" [||] [| file2 |] file2.Size lastWrite2\n\n                let status2 = { status1 with LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(5000L) }\n\n                do!\n                    LocalStateDb.applyStatusIncremental\n                        configuration.GraceStatusFile\n                        status2\n                        [ srcDir2 ]\n                        [\n                            FileSystemDifference.Create Change FileSystemEntryType.File \"src/file.txt\"\n                        ]\n\n                let! readBack2 = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                let srcRead2 =\n                    readBack2.Index.Values\n                    |> Seq.find (fun dv -> dv.RelativePath = \"src\")\n\n                let fileRead2 =\n                    srcRead2.Files\n                    |> Seq.find (fun f -> f.RelativePath = \"src/file.txt\")\n\n                fileRead2.Sha256Hash |> should equal \"hash-2\"\n                fileRead2.IsBinary |> should equal false\n\n                fileRead2.UploadedToObjectStorage\n                |> should equal true\n\n                fileRead2.Size |> should equal 25L\n\n                fileRead2.LastWriteTimeUtc.Ticks\n                |> should equal lastWrite2.Ticks\n            })\n\n    [<Test>]\n    let ``applyStatusIncremental keeps unchanged files when directory version id changes`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(5500L)\n                let lastWrite1 = DateTime(2022, 3, 4, 5, 6, 7, DateTimeKind.Utc)\n                let lastWrite2 = DateTime(2022, 4, 5, 6, 7, 8, DateTimeKind.Utc)\n                let rootId1 = Guid.NewGuid()\n                let rootId2 = Guid.NewGuid()\n\n                let originalLicense = createFileVersion \"LICENSE.md\" \"license-hash-1\" false 10L now lastWrite1\n                let originalReadme = createFileVersion \"README.md\" \"readme-hash-1\" false 20L now lastWrite1\n\n                let rootDir1 =\n                    createDirectoryVersion\n                        configuration\n                        rootId1\n                        Constants.RootDirectoryPath\n                        \"root-hash-1\"\n                        [||]\n                        [| originalLicense; originalReadme |]\n                        (originalLicense.Size + originalReadme.Size)\n                        lastWrite1\n\n                let index1 = GraceIndex()\n                index1.TryAdd(rootId1, rootDir1) |> ignore\n\n                let status1 =\n                    { GraceStatus.Default with\n                        Index = index1\n                        RootDirectoryId = rootId1\n                        RootDirectorySha256Hash = rootDir1.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status1\n\n                let changedLicense = createFileVersion \"LICENSE.md\" \"license-hash-2\" false 15L now lastWrite2\n                let unchangedReadme = createFileVersion \"README.md\" \"readme-hash-1\" false 20L now lastWrite1\n\n                let rootDir2 =\n                    createDirectoryVersion\n                        configuration\n                        rootId2\n                        Constants.RootDirectoryPath\n                        \"root-hash-2\"\n                        [||]\n                        [| changedLicense; unchangedReadme |]\n                        (changedLicense.Size + unchangedReadme.Size)\n                        lastWrite2\n\n                let status2 =\n                    { GraceStatus.Default with\n                        RootDirectoryId = rootId2\n                        RootDirectorySha256Hash = rootDir2.Sha256Hash\n                        LastSuccessfulFileUpload = Instant.FromUnixTimeTicks(5600L)\n                        LastSuccessfulDirectoryVersionUpload = Instant.FromUnixTimeTicks(5600L)\n                    }\n\n                do!\n                    LocalStateDb.applyStatusIncremental\n                        configuration.GraceStatusFile\n                        status2\n                        [ rootDir2 ]\n                        [\n                            FileSystemDifference.Create Change FileSystemEntryType.File \"LICENSE.md\"\n                        ]\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n                readBack.RootDirectoryId |> should equal rootId2\n\n                let rootRead =\n                    readBack.Index.Values\n                    |> Seq.find (fun dv -> dv.RelativePath = Constants.RootDirectoryPath)\n\n                rootRead.Files.Count |> should equal 2\n\n                rootRead.Files\n                |> Seq.exists (fun file -> file.RelativePath = \"LICENSE.md\" && file.Sha256Hash = \"license-hash-2\")\n                |> should equal true\n\n                rootRead.Files\n                |> Seq.exists (fun file -> file.RelativePath = \"README.md\" && file.Sha256Hash = \"readme-hash-1\")\n                |> should equal true\n            })\n\n    [<Test>]\n    let ``applyStatusIncremental delete file removes the row`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(6000L)\n                let lastWrite = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n\n                let file = createFileVersion \"src/delete-me.txt\" \"hash\" false 1L now lastWrite\n\n                let srcDir = createDirectoryVersion configuration srcId \"src\" \"src-hash\" [||] [| file |] file.Size lastWrite\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [| srcId |] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDir) |> ignore\n                index.TryAdd(srcId, srcDir) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n\n                do!\n                    LocalStateDb.applyStatusIncremental\n                        configuration.GraceStatusFile\n                        status\n                        Seq.empty\n                        [\n                            FileSystemDifference.Create Delete FileSystemEntryType.File \"src/delete-me.txt\"\n                        ]\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                readBack.Index.Values\n                |> Seq.collect (fun dv -> dv.Files)\n                |> Seq.exists (fun f -> f.RelativePath = \"src/delete-me.txt\")\n                |> should equal false\n            })\n\n    [<Test>]\n    let ``applyStatusIncremental delete directory removes the directory row`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(7000L)\n                let lastWrite = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let rootId = Guid.NewGuid()\n                let srcId = Guid.NewGuid()\n\n                let srcDir = createDirectoryVersion configuration srcId \"src\" \"src-hash\" [||] [||] 0L lastWrite\n\n                let rootDir = createDirectoryVersion configuration rootId Constants.RootDirectoryPath \"root-hash\" [| srcId |] [||] 0L lastWrite\n\n                let index = GraceIndex()\n                index.TryAdd(rootId, rootDir) |> ignore\n                index.TryAdd(srcId, srcDir) |> ignore\n\n                let status =\n                    { GraceStatus.Default with\n                        Index = index\n                        RootDirectoryId = rootId\n                        RootDirectorySha256Hash = rootDir.Sha256Hash\n                        LastSuccessfulFileUpload = now\n                        LastSuccessfulDirectoryVersionUpload = now\n                    }\n\n                do! LocalStateDb.replaceStatusSnapshot configuration.GraceStatusFile status\n\n                do!\n                    LocalStateDb.applyStatusIncremental\n                        configuration.GraceStatusFile\n                        status\n                        Seq.empty\n                        [\n                            FileSystemDifference.Create Delete FileSystemEntryType.Directory \"src\"\n                        ]\n\n                let! readBack = LocalStateDb.readStatusSnapshot configuration.GraceStatusFile\n\n                readBack.Index.Values\n                |> Seq.exists (fun dv -> dv.RelativePath = \"src\")\n                |> should equal false\n            })\n\n    [<Test>]\n    let ``upsertObjectCache enforces foreign keys`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let lastWrite = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let parentId = Guid.NewGuid()\n                let missingChildId = Guid.NewGuid()\n\n                let parentDir = createDirectoryVersion configuration parentId \"src\" \"parent-hash\" [| missingChildId |] [||] 0L lastWrite\n\n                let operation = fun () -> task { do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ parentDir ] } :> Task\n\n                Assert.ThrowsAsync<InvalidOperationException>(operation)\n                |> ignore\n\n                let! exists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile parentId\n                exists |> should equal false\n            })\n\n    [<Test>]\n    let ``upsertObjectCache supports parent before child order`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(9000L)\n                let lastWrite = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n                let parentId = Guid.NewGuid()\n                let childId = Guid.NewGuid()\n\n                let file = createFileVersion \"src/parent.txt\" \"hash-parent\" false 1L now lastWrite\n                let childDir = createDirectoryVersion configuration childId \"src/child\" \"child-hash\" [||] [||] 0L lastWrite\n                let parentDir = createDirectoryVersion configuration parentId \"src\" \"parent-hash\" [| childId |] [| file |] file.Size lastWrite\n\n                do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ parentDir; childDir ]\n\n                let! parentExists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile parentId\n                parentExists |> should equal true\n\n                let! childExists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile childId\n                childExists |> should equal true\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                let childLinkCount =\n                    executeScalarInt\n                        connection\n                        $\"SELECT COUNT(*) FROM object_cache_directory_children WHERE parent_directory_version_id = '{parentId}' AND child_directory_version_id = '{childId}';\"\n\n                childLinkCount |> should equal 1\n\n                let fileCount =\n                    executeScalarInt\n                        connection\n                        $\"SELECT COUNT(*) FROM object_cache_directory_files WHERE directory_version_id = '{parentId}' AND relative_path = 'src/parent.txt';\"\n\n                fileCount |> should equal 1\n            })\n\n    [<Test>]\n    let ``upsertObjectCache updates referenced child without FK violation`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(9100L)\n                let lastWrite = DateTime(2022, 1, 3, 4, 5, 6, DateTimeKind.Utc)\n                let parentId = Guid.NewGuid()\n                let childId = Guid.NewGuid()\n\n                let childDirV1 = createDirectoryVersion configuration childId \"src/child\" \"child-hash-v1\" [||] [||] 0L lastWrite\n                let parentDir = createDirectoryVersion configuration parentId \"src\" \"parent-hash\" [| childId |] [||] 0L lastWrite\n\n                do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ parentDir; childDirV1 ]\n\n                let childFile = createFileVersion \"src/child/file.txt\" \"child-file-hash-v2\" false 2L now lastWrite\n\n                let childDirV2 =\n                    createDirectoryVersion\n                        configuration\n                        childId\n                        \"src/child\"\n                        \"child-hash-v2\"\n                        [||]\n                        [| childFile |]\n                        childFile.Size\n                        lastWrite\n\n                do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ childDirV2 ]\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                let childLinkCount =\n                    executeScalarInt\n                        connection\n                        $\"SELECT COUNT(*) FROM object_cache_directory_children WHERE parent_directory_version_id = '{parentId}' AND child_directory_version_id = '{childId}';\"\n\n                childLinkCount |> should equal 1\n\n                let childHash =\n                    executeScalarString\n                        connection\n                        $\"SELECT sha256_hash FROM object_cache_directories WHERE directory_version_id = '{childId}';\"\n\n                childHash |> should equal \"child-hash-v2\"\n\n                let childFileCount =\n                    executeScalarInt\n                        connection\n                        $\"SELECT COUNT(*) FROM object_cache_directory_files WHERE directory_version_id = '{childId}' AND relative_path = 'src/child/file.txt';\"\n\n                childFileCount |> should equal 1\n            })\n\n    [<Test>]\n    let ``removeObjectCacheDirectory cascades to children and files`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let now = Instant.FromUnixTimeTicks(9000L)\n                let lastWrite = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n\n                let parentId = Guid.NewGuid()\n                let childId = Guid.NewGuid()\n\n                let file = createFileVersion \"src/cache.txt\" \"hash\" false 1L now lastWrite\n\n                let childDir = createDirectoryVersion configuration childId \"src/child\" \"child-hash\" [||] [||] 0L lastWrite\n\n                let parentDir = createDirectoryVersion configuration parentId \"src\" \"parent-hash\" [| childId |] [| file |] file.Size lastWrite\n\n                do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ childDir; parentDir ]\n\n                do! LocalStateDb.removeObjectCacheDirectory configuration.GraceStatusFile parentId\n\n                use connection = openRawConnection configuration.GraceStatusFile\n\n                use childrenCmd = connection.CreateCommand()\n                childrenCmd.CommandText <- \"SELECT COUNT(*) FROM object_cache_directory_children WHERE parent_directory_version_id = $id;\"\n\n                childrenCmd.Parameters.AddWithValue(\"$id\", parentId.ToString())\n                |> ignore\n\n                let childrenCount = childrenCmd.ExecuteScalar() |> Convert.ToInt32\n                childrenCount |> should equal 0\n\n                use filesCmd = connection.CreateCommand()\n                filesCmd.CommandText <- \"SELECT COUNT(*) FROM object_cache_directory_files WHERE directory_version_id = $id;\"\n\n                filesCmd.Parameters.AddWithValue(\"$id\", parentId.ToString())\n                |> ignore\n\n                let filesCount = filesCmd.ExecuteScalar() |> Convert.ToInt32\n                filesCount |> should equal 0\n\n                let! parentExists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile parentId\n                parentExists |> should equal false\n\n                let! childExists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile childId\n                childExists |> should equal true\n            })\n\n    [<Test>]\n    let ``removeObjectCacheDirectory respects RESTRICT when child is referenced`` () =\n        withTempDir (fun _ configuration ->\n            task {\n                let lastWrite = DateTime(2022, 1, 2, 3, 4, 5, DateTimeKind.Utc)\n\n                let parentId = Guid.NewGuid()\n                let childId = Guid.NewGuid()\n\n                let childDir = createDirectoryVersion configuration childId \"src/child\" \"child-hash\" [||] [||] 0L lastWrite\n\n                let parentDir = createDirectoryVersion configuration parentId \"src\" \"parent-hash\" [| childId |] [||] 0L lastWrite\n\n                do! LocalStateDb.upsertObjectCache configuration.GraceStatusFile [ childDir; parentDir ]\n\n                let operation = fun () -> task { do! LocalStateDb.removeObjectCacheDirectory configuration.GraceStatusFile childId } :> Task\n\n                Assert.ThrowsAsync<SqliteException>(operation)\n                |> ignore\n\n                let! stillExists = LocalStateDb.isDirectoryVersionInObjectCache configuration.GraceStatusFile childId\n                stillExists |> should equal true\n            })\n\n    [<Test>]\n    let ``multi-process writers do not crash or corrupt database`` () =\n        withTempDir (fun _ configuration ->\n            Task.Run<unit> (fun () ->\n                match tryGetWorkerCommand () with\n                | None -> Assert.Ignore(\"Worker binary was not found; build the solution before running this test.\")\n                | Some worker ->\n                    let dbPath = configuration.GraceStatusFile\n                    let rootId = Guid.NewGuid()\n                    let rootHash = \"root-hash\"\n                    let processCount = 4\n                    let iterationsPerProcess = 25\n\n                    let processes =\n                        Array.init processCount (fun _ ->\n                            let startInfo = ProcessStartInfo()\n                            startInfo.FileName <- worker.FileName\n\n                            startInfo.Arguments <- $\"{worker.ArgumentsPrefix} \\\"{dbPath}\\\" {rootId} {rootHash} {iterationsPerProcess}\"\n\n                            startInfo.RedirectStandardOutput <- true\n                            startInfo.RedirectStandardError <- true\n                            startInfo.UseShellExecute <- false\n                            startInfo.CreateNoWindow <- true\n\n                            let proc = new Process()\n                            proc.StartInfo <- startInfo\n\n                            if not (proc.Start()) then failwith \"Failed to start worker process.\"\n\n                            proc)\n\n                    let mutable failed = false\n                    let failures = List<string>()\n\n                    processes\n                    |> Array.iter (fun proc ->\n                        if not failed then\n                            if not (proc.WaitForExit(30000)) then\n                                failed <- true\n\n                                try\n                                    proc.Kill(true)\n                                with\n                                | _ -> ()\n\n                                failures.Add(\"Worker process timed out.\")\n                            elif proc.ExitCode <> 0 then\n                                failed <- true\n                                let stdout = proc.StandardOutput.ReadToEnd()\n                                let stderr = proc.StandardError.ReadToEnd()\n                                failures.Add($\"Worker exit code {proc.ExitCode}. stdout={stdout} stderr={stderr}\")\n\n                        proc.Dispose())\n\n                    if failed then Assert.Fail(String.Join(Environment.NewLine, failures))\n\n                    use connection = openRawConnection dbPath\n                    let integrity = executeScalarString connection \"PRAGMA integrity_check;\"\n                    integrity.ToLowerInvariant() |> should equal \"ok\"\n\n                    let schemaVersion = executeScalarString connection \"SELECT value FROM meta WHERE key = 'schema_version';\"\n                    schemaVersion |> should equal \"2\"\n\n                    let statusMetaCount = executeScalarInt connection \"SELECT COUNT(*) FROM status_meta;\"\n                    statusMetaCount |> should equal 1\n\n                    let meta =\n                        LocalStateDb.readStatusMeta dbPath\n                        |> fun task -> task.GetAwaiter().GetResult()\n\n                    meta.RootDirectoryId |> should equal rootId\n\n                    meta.RootDirectorySha256Hash\n                    |> should equal rootHash\n\n                ()))\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Program.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen NUnit.Framework\nopen System\n\n[<TestFixture>]\nmodule CommandParsingTests =\n    let private withEnvironmentVariable (name: string) (value: string option) (action: unit -> unit) =\n        let original = Environment.GetEnvironmentVariable(name)\n\n        try\n            Environment.SetEnvironmentVariable(name, value |> Option.toObj)\n            action ()\n        finally\n            Environment.SetEnvironmentVariable(name, original)\n\n    [<Test>]\n    let ``top level command returns none for empty args`` () =\n        GraceCommand.tryGetTopLevelCommandFromArgs Array.empty true\n        |> should equal None\n\n    [<Test>]\n    let ``top level command detects command token`` () =\n        GraceCommand.tryGetTopLevelCommandFromArgs [| \"connect\"; \"owner/org/repo\" |] true\n        |> should equal (Some \"connect\")\n\n    [<Test>]\n    let ``top level command skips output option`` () =\n        GraceCommand.tryGetTopLevelCommandFromArgs [| \"--output\"; \"Verbose\"; \"connect\" |] true\n        |> should equal (Some \"connect\")\n\n    [<Test>]\n    let ``top level command skips correlation id option`` () =\n        GraceCommand.tryGetTopLevelCommandFromArgs [| \"-c\"; \"abc123\"; \"connect\" |] true\n        |> should equal (Some \"connect\")\n\n    [<Test>]\n    let ``top level command skips source option`` () =\n        GraceCommand.tryGetTopLevelCommandFromArgs [| \"--source\"; \"codex\"; \"connect\" |] true\n        |> should equal (Some \"connect\")\n\n    [<Test>]\n    let ``top level command honors end of options marker`` () =\n        GraceCommand.tryGetTopLevelCommandFromArgs\n            [|\n                \"--output\"\n                \"Verbose\"\n                \"--\"\n                \"connect\"\n            |]\n            true\n        |> should equal (Some \"connect\")\n\n    [<Test>]\n    let ``resolveInvocationSource prefers explicit source over environment`` () =\n        withEnvironmentVariable Common.SourceEnvironmentVariableName (Some \"env-source\") (fun () ->\n            let parseResult =\n                GraceCommand.rootCommand.Parse(\n                    [|\n                        \"--source\"\n                        \"explicit-source\"\n                        \"history\"\n                        \"show\"\n                    |]\n                )\n\n            parseResult.Errors.Count |> should equal 0\n\n            Common.resolveInvocationSource parseResult\n            |> should equal (Some \"explicit-source\"))\n\n    [<Test>]\n    let ``resolveInvocationSource falls back to environment`` () =\n        withEnvironmentVariable Common.SourceEnvironmentVariableName (Some \"env-source\") (fun () ->\n            let parseResult = GraceCommand.rootCommand.Parse([| \"history\"; \"show\" |])\n            parseResult.Errors.Count |> should equal 0\n\n            Common.resolveInvocationSource parseResult\n            |> should equal (Some \"env-source\"))\n\n    [<Test>]\n    let ``history show accepts source filter option`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                [|\n                    \"history\"\n                    \"show\"\n                    \"--source\"\n                    \"codex\"\n                |]\n            )\n\n        parseResult.Errors.Count |> should equal 0\n\n    [<Test>]\n    let ``history search accepts source filter option`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse [| \"history\"\n                                              \"search\"\n                                              \"workitem\"\n                                              \"--source\"\n                                              \"codex\" |]\n\n        parseResult.Errors.Count |> should equal 0\n\n\nnamespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Utilities\nopen NUnit.Framework\nopen Spectre.Console\nopen System\nopen System.IO\n\n[<NonParallelizable>]\nmodule HelpDoesNotReadConfigTests =\n    let private setAnsiConsoleOutput (writer: TextWriter) =\n        let settings = AnsiConsoleSettings()\n        settings.Out <- AnsiConsoleOutput(writer)\n        AnsiConsole.Console <- AnsiConsole.Create(settings)\n\n    let private runWithCapturedOutput (args: string array) =\n        use writer = new StringWriter()\n        let originalOut = Console.Out\n\n        try\n            Console.SetOut(writer)\n            setAnsiConsoleOutput writer\n            let exitCode = GraceCommand.main args\n            exitCode, writer.ToString()\n        finally\n            Console.SetOut(originalOut)\n            setAnsiConsoleOutput originalOut\n\n    let private captureOutput (action: unit -> unit) =\n        use writer = new StringWriter()\n        let originalOut = Console.Out\n\n        try\n            Console.SetOut(writer)\n            setAnsiConsoleOutput writer\n            action ()\n            writer.ToString()\n        finally\n            Console.SetOut(originalOut)\n            setAnsiConsoleOutput originalOut\n\n\n    let private withTempDir (action: string -> unit) =\n        let tempDir = Path.Combine(Path.GetTempPath(), $\"grace-cli-tests-{Guid.NewGuid():N}\")\n        Directory.CreateDirectory(tempDir) |> ignore\n        let originalDir = Environment.CurrentDirectory\n\n        try\n            Environment.CurrentDirectory <- tempDir\n            resetConfiguration ()\n            action tempDir\n        finally\n            Environment.CurrentDirectory <- originalDir\n\n            if Directory.Exists(tempDir) then\n                try\n                    Directory.Delete(tempDir, true)\n                with\n                | _ -> ()\n\n    let private writeInvalidConfig (root: string) =\n        let graceDir = Path.Combine(root, \".grace\")\n        Directory.CreateDirectory(graceDir) |> ignore\n        File.WriteAllText(Path.Combine(graceDir, \"graceconfig.json\"), \"not json\")\n\n    let private writeValidConfig (root: string) (ownerId: Guid) (orgId: Guid) (repoId: Guid) (branchId: Guid) =\n        let graceDir = Path.Combine(root, \".grace\")\n        Directory.CreateDirectory(graceDir) |> ignore\n        let config = GraceConfiguration()\n        config.OwnerId <- ownerId\n        config.OrganizationId <- orgId\n        config.RepositoryId <- repoId\n        config.BranchId <- branchId\n        let json = serialize config\n        File.WriteAllText(Path.Combine(graceDir, \"graceconfig.json\"), json)\n\n    [<Test>]\n    let ``help works with invalid config`` () =\n        withTempDir (fun root ->\n            writeInvalidConfig root\n\n            let exitCode, _ =\n                runWithCapturedOutput [| \"access\"\n                                         \"grant-role\"\n                                         \"-h\" |]\n\n            exitCode |> should equal 0)\n\n    [<Test>]\n    let ``help works without config`` () =\n        withTempDir (fun _ ->\n            let exitCode, _ =\n                runWithCapturedOutput [| \"access\"\n                                         \"grant-role\"\n                                         \"-h\" |]\n\n            exitCode |> should equal 0)\n\n    [<Test>]\n    let ``help shows symbolic defaults`` () =\n        withTempDir (fun _ ->\n            let exitCode, output =\n                runWithCapturedOutput [| \"access\"\n                                         \"grant-role\"\n                                         \"-h\" |]\n\n            exitCode |> should equal 0\n\n            output\n            |> should contain \"[default: current OwnerId]\"\n\n            output\n            |> should contain \"[default: current OrganizationId]\"\n\n            output\n            |> should contain \"[default: current RepositoryId]\"\n\n            output\n            |> should contain \"[default: current BranchId]\"\n\n            output |> should contain \"[default: new NanoId]\")\n\n    [<Test>]\n    let ``create help rewrites empty guid defaults`` () =\n        withTempDir (fun _ ->\n            let exitCode, output =\n                runWithCapturedOutput [| \"repository\"\n                                         \"create\"\n                                         \"-h\" |]\n\n            exitCode |> should equal 0\n\n            output\n            |> should contain \"[default: current OwnerId]\"\n\n            output\n            |> should contain \"[default: current OrganizationId]\"\n\n            output |> should contain \"[default: new Guid]\"\n\n            output\n            |> should not' (contain \"00000000-0000-0000-0000-0000000000000\"))\n\n    [<Test>]\n    let ``verbose parse result shows resolved ids`` () =\n        withTempDir (fun root ->\n            let ownerId = Guid.NewGuid()\n            let orgId = Guid.NewGuid()\n            let repoId = Guid.NewGuid()\n            let branchId = Guid.NewGuid()\n            writeValidConfig root ownerId orgId repoId branchId\n\n            let parseResult = GraceCommand.rootCommand.Parse([| \"access\"; \"grant-role\" |])\n\n            let output = captureOutput (fun () -> Common.printParseResult parseResult)\n\n            output |> should contain \"Resolved values:\"\n            output |> should contain $\"{ownerId}\"\n            output |> should contain $\"{orgId}\"\n            output |> should contain $\"{repoId}\"\n            output |> should contain $\"{branchId}\")\n\n    [<Test>]\n    let ``getNormalizedIdsAndNames falls back to config ids`` () =\n        withTempDir (fun root ->\n            let ownerId = Guid.NewGuid()\n            let orgId = Guid.NewGuid()\n            let repoId = Guid.NewGuid()\n            let branchId = Guid.NewGuid()\n            writeValidConfig root ownerId orgId repoId branchId\n\n            let parseResult = GraceCommand.rootCommand.Parse([| \"access\"; \"grant-role\" |])\n            let graceIds = Services.getNormalizedIdsAndNames parseResult\n\n            graceIds.OwnerId |> should equal ownerId\n            graceIds.OrganizationId |> should equal orgId\n            graceIds.RepositoryId |> should equal repoId\n            graceIds.BranchId |> should equal branchId)\n\n\nnamespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.Shared.Client\nopen NUnit.Framework\nopen Spectre.Console\nopen System\nopen System.IO\n\n[<NonParallelizable>]\nmodule RootHelpGroupingTests =\n    type private GroupedHelpExpectation = { Args: string array; Headings: string list }\n\n    let private groupedHelpExpectations =\n        [\n            {\n                Args = [| \"repo\"; \"-h\" |]\n                Headings =\n                    [\n                        \"Create and initialize:\"\n                        \"Inspect:\"\n                        \"Configuration:\"\n                        \"Lifecycle:\"\n                    ]\n            }\n            {\n                Args = [| \"branch\"; \"-h\" |]\n                Headings =\n                    [\n                        \"Create and contribute:\"\n                        \"Promotion workflow:\"\n                        \"Inspect:\"\n                        \"Settings:\"\n                        \"Lifecycle:\"\n                    ]\n            }\n            {\n                Args = [| \"owner\"; \"-h\" |]\n                Headings =\n                    [\n                        \"Create and inspect:\"\n                        \"Settings:\"\n                        \"Lifecycle:\"\n                    ]\n            }\n            {\n                Args = [| \"org\"; \"-h\" |]\n                Headings =\n                    [\n                        \"Create and inspect:\"\n                        \"Settings:\"\n                        \"Lifecycle:\"\n                    ]\n            }\n        ]\n\n    let private setAnsiConsoleOutput (writer: TextWriter) =\n        let settings = AnsiConsoleSettings()\n        settings.Out <- AnsiConsoleOutput(writer)\n        AnsiConsole.Console <- AnsiConsole.Create(settings)\n\n    let private runWithCapturedOutput (args: string array) =\n        use writer = new StringWriter()\n        let originalOut = Console.Out\n\n        try\n            Console.SetOut(writer)\n            setAnsiConsoleOutput writer\n            let exitCode = GraceCommand.main args\n            exitCode, writer.ToString()\n        finally\n            Console.SetOut(originalOut)\n            setAnsiConsoleOutput originalOut\n\n    let private withFileBackup (path: string) (action: unit -> unit) =\n        let backupPath = path + \".testbackup\"\n        let hadExisting = File.Exists(path)\n\n        if hadExisting then File.Copy(path, backupPath, true)\n\n        try\n            action ()\n        finally\n            if hadExisting then\n                File.Copy(backupPath, path, true)\n                File.Delete(backupPath)\n            elif File.Exists(path) then\n                File.Delete(path)\n\n    let private withGraceUserFileBackups (action: unit -> unit) =\n        let configPath = UserConfiguration.getUserConfigurationPath ()\n        let historyPath = HistoryStorage.getHistoryFilePath ()\n        let lockPath = HistoryStorage.getHistoryLockPath ()\n\n        withFileBackup configPath (fun () -> withFileBackup historyPath (fun () -> withFileBackup lockPath action))\n\n    let private sliceBetween (text: string) (startText: string) (endText: string) =\n        let startIndex = text.IndexOf(startText, StringComparison.Ordinal)\n        let endIndex = text.IndexOf(endText, StringComparison.Ordinal)\n\n        if startIndex >= 0 && endIndex > startIndex then\n            text.Substring(startIndex, endIndex - startIndex)\n        else\n            text\n\n    [<Test>]\n    let ``root help groups commands`` () =\n        withGraceUserFileBackups (fun () ->\n            let exitCode, output = runWithCapturedOutput [||]\n            exitCode |> should equal 0\n\n            output |> should contain \"Getting started:\"\n            output |> should contain \"Day-to-day development:\"\n            output |> should contain \"Review and promotion:\"\n\n            output\n            |> should contain \"Administration and access:\"\n\n            output |> should contain \"Local utilities:\"\n\n            let gettingStarted = sliceBetween output \"Getting started:\" \"Day-to-day development:\"\n\n            gettingStarted |> should contain \"auth\"\n            gettingStarted |> should contain \"connect\"\n            gettingStarted |> should contain \"config\"\n            gettingStarted |> should not' (contain \"branch\")\n\n            let dayToDay = sliceBetween output \"Day-to-day development:\" \"Review and promotion:\"\n\n            dayToDay |> should contain \"branch\"\n            dayToDay |> should contain \"diff\"\n            dayToDay |> should contain \"directory-version\"\n            dayToDay |> should contain \"watch\"\n            dayToDay |> should not' (contain \"work\")\n\n            let reviewAndPromotion = sliceBetween output \"Review and promotion:\" \"Administration and access:\"\n\n            reviewAndPromotion |> should contain \"workitem\"\n            reviewAndPromotion |> should contain \"review\"\n            reviewAndPromotion |> should contain \"candidate\"\n            reviewAndPromotion |> should contain \"queue\"\n            reviewAndPromotion |> should contain \"agent\"\n\n            reviewAndPromotion\n            |> should contain \"promotion-set\")\n\n    [<Test>]\n    let ``subcommand help is not grouped`` () =\n        withGraceUserFileBackups (fun () ->\n            let exitCode, output =\n                runWithCapturedOutput [| \"branch\"\n                                         \"-h\" |]\n\n            exitCode |> should equal 0\n            output |> should not' (contain \"Getting started:\")\n\n            output\n            |> should not' (contain \"Day-to-day development:\")\n\n            output\n            |> should not' (contain \"Review and promotion:\")\n\n            output\n            |> should not' (contain \"Administration and access:\")\n\n            output |> should not' (contain \"Local utilities:\"))\n\n    [<Test>]\n    let ``selected command helps are grouped`` () =\n        withGraceUserFileBackups (fun () ->\n            for expectation in groupedHelpExpectations do\n                let exitCode, output = runWithCapturedOutput expectation.Args\n                exitCode |> should equal 0\n\n                for heading in expectation.Headings do\n                    output |> should contain heading)\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Program.fs",
    "content": "namespace Grace.CLI.Tests\n\nmodule Program =\n    [<EntryPoint>]\n    let main _ = 0\n"
  },
  {
    "path": "src/Grace.CLI.Tests/PromotionSet.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen NUnit.Framework\nopen System\nopen System.IO\n\n[<NonParallelizable>]\nmodule PromotionSetCommandTests =\n    let private ownerId = Guid.NewGuid()\n    let private organizationId = Guid.NewGuid()\n    let private repositoryId = Guid.NewGuid()\n\n    let private withIds (args: string array) =\n        Array.append\n            args\n            [|\n                \"--owner-id\"\n                ownerId.ToString()\n                \"--organization-id\"\n                organizationId.ToString()\n                \"--repository-id\"\n                repositoryId.ToString()\n            |]\n\n    let private withIdsAndSilent (args: string array) =\n        args\n        |> Array.append [| \"--output\"; \"Silent\" |]\n        |> withIds\n\n    [<Test>]\n    let ``promotion-set create rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"create\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\"\n                                    \"--target-branch-id\"\n                                    (Guid.NewGuid().ToString()) |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set create rejects invalid target branch id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"create\"\n                                    \"--target-branch-id\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set get rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"get\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set get-events rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"get-events\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set update-input-promotions rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"update-input-promotions\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\"\n                                    \"--promotion-pointers-file\"\n                                    \"pointers.json\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set update-input-promotions rejects missing pointers file`` () =\n        let missingFile = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.json\")\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"update-input-promotions\"\n                                    \"--promotion-set\"\n                                    (Guid.NewGuid().ToString())\n                                    \"--promotion-pointers-file\"\n                                    missingFile |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set update-input-promotions rejects malformed pointers file`` () =\n        let tempFile = Path.GetTempFileName()\n\n        try\n            File.WriteAllText(tempFile, \"this is not json\")\n\n            let parseResult =\n                GraceCommand.rootCommand.Parse(\n                    withIdsAndSilent [| \"promotion-set\"\n                                        \"update-input-promotions\"\n                                        \"--promotion-set\"\n                                        (Guid.NewGuid().ToString())\n                                        \"--promotion-pointers-file\"\n                                        tempFile |]\n                )\n\n            let exitCode = parseResult.Invoke()\n            exitCode |> should equal -1\n        finally\n            if File.Exists tempFile then File.Delete tempFile\n\n    [<Test>]\n    let ``promotion-set recompute rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"recompute\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set apply rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"apply\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set delete rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"delete\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set conflicts show rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"conflicts\"\n                                    \"show\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set conflicts resolve rejects invalid step id`` () =\n        let tempFile = Path.GetTempFileName()\n\n        try\n            File.WriteAllText(tempFile, \"[]\")\n\n            let parseResult =\n                GraceCommand.rootCommand.Parse(\n                    withIdsAndSilent [| \"promotion-set\"\n                                        \"conflicts\"\n                                        \"resolve\"\n                                        \"--promotion-set\"\n                                        (Guid.NewGuid().ToString())\n                                        \"--step\"\n                                        \"not-a-guid\"\n                                        \"--decisions-file\"\n                                        tempFile |]\n                )\n\n            let exitCode = parseResult.Invoke()\n            exitCode |> should equal -1\n        finally\n            if File.Exists tempFile then File.Delete tempFile\n\n    [<Test>]\n    let ``promotion-set conflicts resolve rejects missing decisions file`` () =\n        let missingFile = Path.Combine(Path.GetTempPath(), $\"{Guid.NewGuid()}.json\")\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"promotion-set\"\n                                    \"conflicts\"\n                                    \"resolve\"\n                                    \"--promotion-set\"\n                                    (Guid.NewGuid().ToString())\n                                    \"--step\"\n                                    (Guid.NewGuid().ToString())\n                                    \"--decisions-file\"\n                                    missingFile |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``promotion-set conflicts resolve rejects malformed decisions file`` () =\n        let tempFile = Path.GetTempFileName()\n\n        try\n            File.WriteAllText(tempFile, \"this is not json\")\n\n            let parseResult =\n                GraceCommand.rootCommand.Parse(\n                    withIdsAndSilent [| \"promotion-set\"\n                                        \"conflicts\"\n                                        \"resolve\"\n                                        \"--promotion-set\"\n                                        (Guid.NewGuid().ToString())\n                                        \"--step\"\n                                        (Guid.NewGuid().ToString())\n                                        \"--decisions-file\"\n                                        tempFile |]\n                )\n\n            let exitCode = parseResult.Invoke()\n            exitCode |> should equal -1\n        finally\n            if File.Exists tempFile then File.Delete tempFile\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Queue.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen NUnit.Framework\nopen System\n\n[<NonParallelizable>]\nmodule QueueCommandTests =\n    let private ownerId = Guid.NewGuid()\n    let private organizationId = Guid.NewGuid()\n    let private repositoryId = Guid.NewGuid()\n    let private branchId = Guid.NewGuid()\n\n    let private withIds (args: string array) =\n        Array.append\n            args\n            [|\n                \"--owner-id\"\n                ownerId.ToString()\n                \"--organization-id\"\n                organizationId.ToString()\n                \"--repository-id\"\n                repositoryId.ToString()\n            |]\n\n    let private withIdsAndSilent (args: string array) =\n        args\n        |> Array.append [| \"--output\"; \"Silent\" |]\n        |> withIds\n\n    [<Test>]\n    let ``queue enqueue rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"queue\"\n                                    \"enqueue\"\n                                    \"--branch-id\"\n                                    branchId.ToString()\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``queue enqueue rejects invalid work item id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"queue\"\n                                    \"enqueue\"\n                                    \"--branch-id\"\n                                    branchId.ToString()\n                                    \"--work\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``queue enqueue accepts numeric work item identifier`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIds [| \"queue\"\n                           \"enqueue\"\n                           \"--branch-id\"\n                           branchId.ToString()\n                           \"--work\"\n                           \"42\" |]\n            )\n\n        parseResult.Errors.Count |> should equal 0\n\n    [<Test>]\n    let ``queue dequeue rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"queue\"\n                                    \"dequeue\"\n                                    \"--branch-id\"\n                                    branchId.ToString()\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``queue retry command is unavailable`` () =\n        let parseResult = GraceCommand.rootCommand.Parse([| \"queue\"; \"retry\" |])\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n\n        let hasRetryError =\n            parseResult.Errors\n            |> Seq.exists (fun error ->\n                error.Message.Contains(\"Unrecognized command or argument 'retry'\", StringComparison.OrdinalIgnoreCase))\n\n        Assert.That(hasRetryError, Is.True)\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Review.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.CLI.Command\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Types.Policy\nopen Grace.Types.PromotionSet\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.IO\nopen System.Threading.Tasks\n\n[<NonParallelizable>]\nmodule ReviewCommandTests =\n    let private ownerId = Guid.NewGuid()\n    let private organizationId = Guid.NewGuid()\n    let private repositoryId = Guid.NewGuid()\n\n    let private graceIds =\n        { GraceIds.Default with\n            OwnerId = ownerId\n            OwnerIdString = ownerId.ToString()\n            OrganizationId = organizationId\n            OrganizationIdString = organizationId.ToString()\n            RepositoryId = repositoryId\n            RepositoryIdString = repositoryId.ToString()\n            CorrelationId = \"corr-review\"\n        }\n\n    let private withIds (args: string array) =\n        Array.append\n            args\n            [|\n                \"--owner-id\"\n                ownerId.ToString()\n                \"--organization-id\"\n                organizationId.ToString()\n                \"--repository-id\"\n                repositoryId.ToString()\n            |]\n\n    let private withIdsAndSilent (args: string array) =\n        args\n        |> Array.append [| \"--output\"; \"Silent\" |]\n        |> withIds\n\n    let private invokeWithCapturedConsole (parseResult: System.CommandLine.ParseResult) =\n        use standardOutWriter = new StringWriter()\n        use standardErrorWriter = new StringWriter()\n        let originalOut = Console.Out\n        let originalError = Console.Error\n\n        try\n            Console.SetOut(standardOutWriter)\n            Console.SetError(standardErrorWriter)\n            let exitCode = parseResult.Invoke()\n            exitCode, standardOutWriter.ToString(), standardErrorWriter.ToString()\n        finally\n            Console.SetOut(originalOut)\n            Console.SetError(originalError)\n\n    let private parseCheckpoint (promotionSetId: Guid) (extraArgs: string array) =\n        let baseArgs =\n            [|\n                \"review\"\n                \"checkpoint\"\n                \"--promotion-set\"\n                promotionSetId.ToString()\n                \"--reference-id\"\n                Guid.NewGuid().ToString()\n            |]\n\n        GraceCommand.rootCommand.Parse(withIdsAndSilent (Array.append baseArgs extraArgs))\n\n    [<Test>]\n    let ``review open rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"review\"\n                                    \"open\"\n                                    \"--promotion-set\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``review checkpoint rejects invalid reference id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"review\"\n                                    \"checkpoint\"\n                                    \"--promotion-set\"\n                                    Guid.NewGuid().ToString()\n                                    \"--reference-id\"\n                                    \"not-a-guid\"\n                                    \"--policy-snapshot-id\"\n                                    \"snapshot\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``review resolve requires resolution state`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"review\"\n                                    \"resolve\"\n                                    \"--promotion-set\"\n                                    Guid.NewGuid().ToString()\n                                    \"--finding-id\"\n                                    Guid.NewGuid().ToString() |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``review resolve rejects approve and request changes together`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"review\"\n                                    \"resolve\"\n                                    \"--promotion-set\"\n                                    Guid.NewGuid().ToString()\n                                    \"--finding-id\"\n                                    Guid.NewGuid().ToString()\n                                    \"--approve\"\n                                    \"--request-changes\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``review delta command is unavailable`` () =\n        let parseResult = GraceCommand.rootCommand.Parse([| \"review\"; \"delta\" |])\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n\n        let hasDeltaError =\n            parseResult.Errors\n            |> Seq.exists (fun error -> error.Message.Contains(\"Unrecognized command or argument 'delta'\", StringComparison.OrdinalIgnoreCase))\n\n        Assert.That(hasDeltaError, Is.True)\n\n    [<Test>]\n    let ``review checkpoint uses explicit policy snapshot when provided`` () =\n        let promotionSetId = Guid.NewGuid()\n\n        let parseResult =\n            parseCheckpoint\n                promotionSetId\n                [|\n                    \"--policy-snapshot-id\"\n                    \"snapshot-explicit\"\n                |]\n\n        let promotionSet = { PromotionSetDto.Default with PromotionSetId = promotionSetId; TargetBranchId = Guid.NewGuid() }\n\n        let mutable getPromotionSetCalled = false\n        let mutable policyCalled = false\n\n        let getPromotionSet (_: Parameters.PromotionSet.GetPromotionSetParameters) =\n            getPromotionSetCalled <- true\n            Task.FromResult(Ok(GraceReturnValue.Create promotionSet graceIds.CorrelationId))\n\n        let getPolicy (_: Parameters.Policy.GetPolicyParameters) =\n            policyCalled <- true\n\n            Task.FromResult(\n                Ok(GraceReturnValue.Create (Some { PolicySnapshot.Default with PolicySnapshotId = PolicySnapshotId \"snapshot-policy\" }) graceIds.CorrelationId)\n            )\n\n        let result =\n            ReviewCommand.resolvePolicySnapshotIdWith getPromotionSet getPolicy parseResult graceIds promotionSetId\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        match result with\n        | Ok snapshotId -> snapshotId |> should equal \"snapshot-explicit\"\n        | Error error -> Assert.Fail error.Error\n\n        Assert.That(getPromotionSetCalled, Is.False)\n        Assert.That(policyCalled, Is.False)\n\n    [<Test>]\n    let ``review checkpoint falls back to policy snapshot when promotion set snapshot is missing`` () =\n        let promotionSetId = Guid.NewGuid()\n        let targetBranchId = Guid.NewGuid()\n        let parseResult = parseCheckpoint promotionSetId [||]\n\n        let promotionSet = { PromotionSetDto.Default with PromotionSetId = promotionSetId; TargetBranchId = targetBranchId }\n\n        let getPromotionSet (_: Parameters.PromotionSet.GetPromotionSetParameters) =\n            Task.FromResult(Ok(GraceReturnValue.Create promotionSet graceIds.CorrelationId))\n\n        let policySnapshot = { PolicySnapshot.Default with PolicySnapshotId = PolicySnapshotId \"snapshot-policy\"; TargetBranchId = targetBranchId }\n\n        let getPolicy (_: Parameters.Policy.GetPolicyParameters) = Task.FromResult(Ok(GraceReturnValue.Create (Some policySnapshot) graceIds.CorrelationId))\n\n        let result =\n            ReviewCommand.resolvePolicySnapshotIdWith getPromotionSet getPolicy parseResult graceIds promotionSetId\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        match result with\n        | Ok snapshotId -> snapshotId |> should equal \"snapshot-policy\"\n        | Error error -> Assert.Fail error.Error\n\n    [<Test>]\n    let ``candidate get rejects invalid candidate id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"candidate\"\n                                    \"get\"\n                                    \"--candidate\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``candidate required actions rejects invalid candidate id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"candidate\"\n                                    \"required-actions\"\n                                    \"--candidate\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``candidate gate rerun requires gate option`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"candidate\"\n                                    \"gate\"\n                                    \"rerun\"\n                                    \"--candidate\"\n                                    Guid.NewGuid().ToString() |]\n            )\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n        let exitCode, _, standardError = invokeWithCapturedConsole parseResult\n        exitCode |> should equal 1\n        standardError |> should contain \"Option '--gate' is required.\"\n\n    [<Test>]\n    let ``candidate parser normalizes guid candidate id`` () =\n        let candidateId = Guid.NewGuid()\n\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"candidate\"\n                                    \"get\"\n                                    \"--candidate\"\n                                    $\"  {candidateId.ToString().ToUpperInvariant()}  \" |]\n            )\n\n        let parsed = CandidateCommand.tryParseCandidateId (parseResult.GetValue(\"--candidate\")) parseResult\n\n        match parsed with\n        | Ok canonical -> canonical |> should equal (candidateId.ToString())\n        | Error error -> Assert.Fail error.Error\n\n    let private createReportSection (section: string) (title: string) (sourceState: string) (entries: (string * string list) list) =\n        let reportSection = ReviewReportSection()\n        reportSection.Section <- section\n        reportSection.Title <- title\n        reportSection.SourceState <- sourceState\n\n        reportSection.Entries <-\n            entries\n            |> List.map (fun (key, values) ->\n                let entry = ReviewReportEntry()\n                entry.Key <- key\n                entry.Values <- values\n                entry)\n\n        reportSection\n\n    let private createSampleReviewReport () =\n        let report = ReviewReportResult()\n        report.ReviewReportSchemaVersion <- ReviewReportSchema.Version\n        report.SectionOrder <- ReviewReportSections.Ordered\n\n        report.Sections <-\n            [\n                createReportSection\n                    ReviewReportSections.BlockingReasonsAndNextActions\n                    \"Blocking reasons and next actions\"\n                    \"Inferred\"\n                    [\n                        \"Blockers\",\n                        [\n                            \"High|review-notes-and-checkpoint|Review findings require resolution before candidate promotion can continue.\"\n                        ]\n                        \"NextActions\",\n                        [\n                            \"grace review open --promotion-set 00000000-0000-0000-0000-000000000001\"\n                        ]\n                    ]\n                createReportSection\n                    ReviewReportSections.CandidateAndPromotionSet\n                    \"Candidate and PromotionSet identity or status\"\n                    \"Authoritative\"\n                    [\n                        \"CandidateId\",\n                        [\n                            \"00000000-0000-0000-0000-000000000001\"\n                        ]\n                        \"PromotionSetId\",\n                        [\n                            \"00000000-0000-0000-0000-000000000001\"\n                        ]\n                    ]\n                createReportSection\n                    ReviewReportSections.QueueAndRequiredActions\n                    \"Queue state and required actions\"\n                    \"Authoritative\"\n                    [\n                        \"QueueState\", [ \"Ready\" ]\n                        \"RequiredActions\",\n                        [\n                            \"ResolveFindings\"\n                            \"RetryComputation\"\n                        ]\n                    ]\n            ]\n\n        report\n\n    [<Test>]\n    let ``review report show rejects invalid candidate id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"review\"\n                                    \"report\"\n                                    \"show\"\n                                    \"--candidate\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``review report export requires format and output file`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"review\"\n                                    \"report\"\n                                    \"export\"\n                                    \"--candidate\"\n                                    Guid.NewGuid().ToString() |]\n            )\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n        let exitCode, _, standardError = invokeWithCapturedConsole parseResult\n        exitCode |> should equal 1\n        standardError |> should contain \"Option '--format' is required.\"\n        standardError |> should contain \"Option '--output-file' is required.\"\n\n    [<Test>]\n    let ``review report json serialization includes schema version`` () =\n        let report = createSampleReviewReport ()\n        let json = ReviewCommand.serializeReviewReportJson report\n        Assert.That(json, Does.Contain(\"\\\"ReviewReportSchemaVersion\\\": \\\"1.0\\\"\"))\n\n    [<Test>]\n    let ``review report normalization enforces section order`` () =\n        let report = createSampleReviewReport ()\n        let normalized = ReviewCommand.normalizeReviewReportForOutput report\n\n        normalized.Sections\n        |> List.map (fun section -> section.Section)\n        |> should\n            equal\n            [\n                ReviewReportSections.CandidateAndPromotionSet\n                ReviewReportSections.QueueAndRequiredActions\n                ReviewReportSections.BlockingReasonsAndNextActions\n            ]\n\n    [<Test>]\n    let ``review report markdown rendering is deterministic`` () =\n        let report = createSampleReviewReport ()\n        let first = ReviewCommand.renderReviewReportMarkdown report\n        let second = ReviewCommand.renderReviewReportMarkdown report\n        first |> should equal second\n"
  },
  {
    "path": "src/Grace.CLI.Tests/Watch.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsUnit\nopen Grace.CLI\nopen Grace.CLI.Command\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen NUnit.Framework\nopen Spectre.Console\nopen System\nopen System.IO\n\n[<NonParallelizable>]\nmodule WatchTests =\n    let private setAnsiConsoleOutput (writer: TextWriter) =\n        let settings = AnsiConsoleSettings()\n        settings.Out <- AnsiConsoleOutput(writer)\n        AnsiConsole.Console <- AnsiConsole.Create(settings)\n\n    let private runWithCapturedOutput (args: string array) =\n        use writer = new StringWriter()\n        let originalOut = Console.Out\n\n        try\n            Console.SetOut(writer)\n            setAnsiConsoleOutput writer\n            let parseResult = GraceCommand.rootCommand.Parse(args)\n            let exitCode = parseResult.InvokeAsync().Result\n            exitCode, writer.ToString()\n        finally\n            Console.SetOut(originalOut)\n            setAnsiConsoleOutput originalOut\n\n    let private withEnv (name: string) (value: string option) (action: unit -> unit) =\n        let original = Environment.GetEnvironmentVariable(name)\n\n        match value with\n        | Some v -> Environment.SetEnvironmentVariable(name, v)\n        | None -> Environment.SetEnvironmentVariable(name, null)\n\n        try\n            action ()\n        finally\n            Environment.SetEnvironmentVariable(name, original)\n\n    let private withClearedEnvVars (names: string list) (action: unit -> unit) =\n        let rec run remaining =\n            match remaining with\n            | [] -> action ()\n            | head :: tail -> withEnv head None (fun () -> run tail)\n\n        run names\n\n    let private clearWatchAuthEnv (action: unit -> unit) =\n        withClearedEnvVars\n            [\n                Constants.EnvironmentVariables.GraceToken\n                Constants.EnvironmentVariables.GraceTokenFile\n                Constants.EnvironmentVariables.GraceAuthOidcAuthority\n                Constants.EnvironmentVariables.GraceAuthOidcAudience\n                Constants.EnvironmentVariables.GraceAuthOidcCliClientId\n                Constants.EnvironmentVariables.GraceAuthOidcCliRedirectPort\n                Constants.EnvironmentVariables.GraceAuthOidcCliScopes\n                Constants.EnvironmentVariables.GraceAuthOidcM2mClientId\n                Constants.EnvironmentVariables.GraceAuthOidcM2mClientSecret\n                Constants.EnvironmentVariables.GraceAuthOidcM2mScopes\n                Constants.EnvironmentVariables.GraceServerUri\n            ]\n            action\n\n    let private withTempRepo (action: string -> unit) =\n        let tempDir = Path.Combine(Path.GetTempPath(), $\"grace-watch-tests-{Guid.NewGuid():N}\")\n        let graceDir = Path.Combine(tempDir, Constants.GraceConfigDirectory)\n        let configPath = Path.Combine(graceDir, Constants.GraceConfigFileName)\n        Directory.CreateDirectory(graceDir) |> ignore\n        File.WriteAllText(configPath, \"{}\")\n\n        let originalDir = Environment.CurrentDirectory\n\n        try\n            Environment.CurrentDirectory <- tempDir\n            resetConfiguration ()\n            action tempDir\n        finally\n            resetConfiguration ()\n            Environment.CurrentDirectory <- originalDir\n\n            if Directory.Exists(tempDir) then\n                try\n                    Directory.Delete(tempDir, true)\n                with\n                | _ -> ()\n\n    [<Test>]\n    let ``resolveSignalRAccessTokenResult returns token when present`` () =\n        let result = Watch.resolveSignalRAccessTokenResult (Ok(Some \"token-value\"))\n\n        match result with\n        | Ok token -> token |> should equal \"token-value\"\n        | Error error -> Assert.Fail($\"Expected token result, got error: {error}\")\n\n    [<Test>]\n    let ``resolveSignalRAccessTokenResult errors when token is missing`` () =\n        let result = Watch.resolveSignalRAccessTokenResult (Ok None)\n\n        match result with\n        | Ok token -> Assert.Fail($\"Expected missing token error, got token: {token}\")\n        | Error error -> error |> should contain \"No access token is available.\"\n\n    [<Test>]\n    let ``resolveSignalRAccessTokenResult includes underlying auth error`` () =\n        let result = Watch.resolveSignalRAccessTokenResult (Error \"test error\")\n\n        match result with\n        | Ok token -> Assert.Fail($\"Expected auth error, got token: {token}\")\n        | Error error ->\n            error |> should contain \"Unable to acquire an access token for SignalR notifications:\"\n            error |> should contain \"test error\"\n\n    [<Test>]\n    let ``watch exits with auth guidance when no token is configured`` () =\n        withTempRepo (fun _ ->\n            clearWatchAuthEnv (fun () ->\n                let exitCode, output = runWithCapturedOutput [| \"watch\" |]\n                if exitCode <> -1 then\n                    Assert.Fail($\"Expected watch to exit with -1 when auth is missing. Actual: {exitCode}.{Environment.NewLine}Output:{Environment.NewLine}{output}\")\n                output |> should contain \"Unable to acquire an access token for SignalR\"\n                output |> should contain \"Authentication is not configured.\"\n            ))\n"
  },
  {
    "path": "src/Grace.CLI.Tests/WorkItem.CLI.Tests.fs",
    "content": "namespace Grace.CLI.Tests\n\nopen FsCheck.NUnit\nopen FsUnit\nopen Grace.CLI\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<NonParallelizable>]\nmodule WorkItemCommandTests =\n    let private ownerId = Guid.NewGuid()\n    let private organizationId = Guid.NewGuid()\n    let private repositoryId = Guid.NewGuid()\n\n    let private withIds (args: string array) =\n        Array.append\n            args\n            [|\n                \"--owner-id\"\n                ownerId.ToString()\n                \"--organization-id\"\n                organizationId.ToString()\n                \"--repository-id\"\n                repositoryId.ToString()\n            |]\n\n    let private withIdsAndSilent (args: string array) =\n        args\n        |> Array.append [| \"--output\"; \"Silent\" |]\n        |> withIds\n\n    let private assertParsesWithoutErrors (args: string array) =\n        let parseResult = GraceCommand.rootCommand.Parse(args)\n        parseResult.Errors.Count |> should equal 0\n\n    let private buildAttachArgs (noun: string) (attachmentType: string) (workItemIdentifier: string) (extraArgs: string array) =\n        [|\n            noun\n            \"attach\"\n            attachmentType\n            workItemIdentifier\n            yield! extraArgs\n        |]\n\n    let private buildAttachmentsArgs (noun: string) (verb: string) (workItemIdentifier: string) (extraArgs: string array) =\n        [|\n            noun\n            \"attachments\"\n            verb\n            workItemIdentifier\n            yield! extraArgs\n        |]\n\n    [<Test>]\n    let ``workitem create parses`` () =\n        assertParsesWithoutErrors (\n            withIds [| \"workitem\"\n                       \"create\"\n                       \"--title\"\n                       \"Test work\" |]\n        )\n\n    [<Test>]\n    let ``work alias still parses`` () =\n        assertParsesWithoutErrors (\n            withIds [| \"work\"\n                       \"create\"\n                       \"--title\"\n                       \"Alias still works\" |]\n        )\n\n    [<TestCase(\"workitem\")>]\n    [<TestCase(\"work\")>]\n    [<TestCase(\"work-item\")>]\n    [<TestCase(\"wi\")>]\n    let ``all work item command aliases parse create`` (commandAlias: string) =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"create\"\n                       \"--title\"\n                       \"Alias command\" |]\n        )\n\n    [<TestCase(\"workitem\", \"40\")>]\n    [<TestCase(\"workitem\", \"9e4c0f72-9b4f-4f28-8d8f-d7d73ec4f6fd\")>]\n    [<TestCase(\"wi\", \"41\")>]\n    [<TestCase(\"work-item\", \"4f2e4a67-4b51-4c7a-b866-f82638852e9d\")>]\n    let ``workitem link ref parses for guid and numeric work item identifiers`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"link\"\n                       \"ref\"\n                       workItemIdentifier\n                       Guid.NewGuid().ToString() |]\n        )\n\n    [<TestCase(\"workitem\", \"42\")>]\n    [<TestCase(\"workitem\", \"f4b59cad-8d03-4a39-b1ff-8bcaf3e609d6\")>]\n    [<TestCase(\"wi\", \"43\")>]\n    [<TestCase(\"work\", \"4caedab7-2472-4df2-a948-94e8e89f2f77\")>]\n    let ``workitem link prset parses for guid and numeric work item identifiers`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"link\"\n                       \"prset\"\n                       workItemIdentifier\n                       Guid.NewGuid().ToString() |]\n        )\n\n    [<TestCase(\"workitem\", \"summary\")>]\n    [<TestCase(\"workitem\", \"prompt\")>]\n    [<TestCase(\"workitem\", \"notes\")>]\n    [<TestCase(\"wi\", \"summary\")>]\n    [<TestCase(\"work-item\", \"prompt\")>]\n    let ``workitem attach parses with file text and stdin modes`` (commandAlias: string, attachmentType: string) =\n        let workItemIdentifier = Guid.NewGuid().ToString()\n\n        let fileArgs =\n            buildAttachArgs\n                commandAlias\n                attachmentType\n                workItemIdentifier\n                [|\n                    \"--file\"\n                    \"C:\\\\temp\\\\attachment.txt\"\n                |]\n            |> withIds\n\n        let textArgs =\n            buildAttachArgs commandAlias attachmentType workItemIdentifier [| \"--text\"; \"inline content\" |]\n            |> withIds\n\n        let stdinArgs =\n            buildAttachArgs commandAlias attachmentType workItemIdentifier [| \"--stdin\" |]\n            |> withIds\n\n        assertParsesWithoutErrors fileArgs\n        assertParsesWithoutErrors textArgs\n        assertParsesWithoutErrors stdinArgs\n\n    [<TestCase(\"workitem\", \"44\")>]\n    [<TestCase(\"workitem\", \"02f8563a-8508-4fdb-a55f-3a326d2be3e0\")>]\n    [<TestCase(\"work\", \"45\")>]\n    [<TestCase(\"wi\", \"d0ac8efe-5f60-4a4f-9563-30dfd8fd2f3e\")>]\n    let ``workitem links list parses for guid and numeric work item identifiers`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"links\"\n                       \"list\"\n                       workItemIdentifier |]\n        )\n\n    [<TestCase(\"workitem\", \"52\")>]\n    [<TestCase(\"workitem\", \"9dfdb7a5-27f6-4fd8-95cf-f5e4f2b22803\")>]\n    [<TestCase(\"work\", \"53\")>]\n    [<TestCase(\"wi\", \"9761ae11-ec40-4c2a-a6e7-e13001642f8e\")>]\n    let ``workitem attachments list parses for guid and numeric work item identifiers`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            buildAttachmentsArgs commandAlias \"list\" workItemIdentifier [||]\n            |> withIds\n        )\n\n    [<TestCase(\"workitem\", \"summary\", \"54\", true)>]\n    [<TestCase(\"workitem\", \"prompt\", \"36f74308-b75c-4a2a-bf2f-fe3e2036b232\", false)>]\n    [<TestCase(\"work\", \"notes\", \"55\", true)>]\n    [<TestCase(\"wi\", \"summary\", \"16dd0b9b-00eb-480f-bf9c-8cfdad68f249\", false)>]\n    let ``workitem attachments show parses with type and latest options``\n        (\n            commandAlias: string,\n            attachmentType: string,\n            workItemIdentifier: string,\n            includeLatest: bool\n        )\n        =\n        let extraArgs = ResizeArray<string>()\n        extraArgs.Add(\"--type\")\n        extraArgs.Add(attachmentType)\n\n        if includeLatest then extraArgs.Add(\"--latest\")\n\n        assertParsesWithoutErrors (\n            buildAttachmentsArgs commandAlias \"show\" workItemIdentifier (extraArgs.ToArray())\n            |> withIds\n        )\n\n    [<TestCase(\"workitem\", \"56\")>]\n    [<TestCase(\"workitem\", \"f4cf5f70-f4ff-461f-8f2d-5be9734b5b7f\")>]\n    [<TestCase(\"work-item\", \"57\")>]\n    [<TestCase(\"wi\", \"b87d5076-6467-4ef6-93f5-8ee7f014295c\")>]\n    let ``workitem attachments download parses with artifact id and output file`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            buildAttachmentsArgs\n                commandAlias\n                \"download\"\n                workItemIdentifier\n                [|\n                    \"--artifact-id\"\n                    Guid.NewGuid().ToString()\n                    \"--output-file\"\n                    \"C:\\\\temp\\\\attachment.bin\"\n                |]\n            |> withIds\n        )\n\n    [<TestCase(\"workitem\", \"46\")>]\n    [<TestCase(\"workitem\", \"f4bc1e7f-5d7a-4f54-a80f-e2d36dc19374\")>]\n    [<TestCase(\"wi\", \"47\")>]\n    let ``workitem links remove ref parses for guid and numeric work item identifiers`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"links\"\n                       \"remove\"\n                       \"ref\"\n                       workItemIdentifier\n                       Guid.NewGuid().ToString() |]\n        )\n\n    [<TestCase(\"workitem\", \"48\")>]\n    [<TestCase(\"workitem\", \"8b684baf-3fe4-4829-b2e8-a67d8c63d1b6\")>]\n    [<TestCase(\"work-item\", \"49\")>]\n    let ``workitem links remove prset parses for guid and numeric work item identifiers`` (commandAlias: string, workItemIdentifier: string) =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"links\"\n                       \"remove\"\n                       \"prset\"\n                       workItemIdentifier\n                       Guid.NewGuid().ToString() |]\n        )\n\n    [<TestCase(\"workitem\", \"summary\", \"50\")>]\n    [<TestCase(\"workitem\", \"prompt\", \"6a635cbe-19ce-4e5f-a0fd-f1c1d1d468ea\")>]\n    [<TestCase(\"wi\", \"notes\", \"51\")>]\n    [<TestCase(\"work\", \"summary\", \"fdb37dfa-699d-4f8f-80f0-6e2eb6222596\")>]\n    let ``workitem links remove artifact-type aliases parse for guid and numeric work item identifiers``\n        (\n            commandAlias: string,\n            linkType: string,\n            workItemIdentifier: string\n        )\n        =\n        assertParsesWithoutErrors (\n            withIds [| commandAlias\n                       \"links\"\n                       \"remove\"\n                       linkType\n                       workItemIdentifier |]\n        )\n\n    [<Test>]\n    let ``workitem show rejects invalid work item identifier`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"workitem\"\n                                    \"show\"\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``workitem link ref rejects invalid reference id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"workitem\"\n                                    \"link\"\n                                    \"ref\"\n                                    Guid.NewGuid().ToString()\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``work link prset rejects invalid promotion set id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"work\"\n                                    \"link\"\n                                    \"prset\"\n                                    Guid.NewGuid().ToString()\n                                    \"not-a-guid\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``work link artifact command is unavailable`` () =\n        let parseResult = GraceCommand.rootCommand.Parse([| \"work\"; \"link\"; \"artifact\" |])\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n\n        let hasArtifactError =\n            parseResult.Errors\n            |> Seq.exists (fun error -> error.Message.Contains(\"Unrecognized command or argument 'artifact'\", StringComparison.OrdinalIgnoreCase))\n\n        Assert.That(hasArtifactError, Is.True)\n\n    [<Test>]\n    let ``workitem attach summary requires exactly one input source`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"workitem\"\n                                    \"attach\"\n                                    \"summary\"\n                                    Guid.NewGuid().ToString() |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``workitem attach summary rejects multiple input sources`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"workitem\"\n                                    \"attach\"\n                                    \"summary\"\n                                    Guid.NewGuid().ToString()\n                                    \"--text\"\n                                    \"hello\"\n                                    \"--stdin\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``workitem attachments show rejects invalid type values during parse`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIds [| \"workitem\"\n                           \"attachments\"\n                           \"show\"\n                           Guid.NewGuid().ToString()\n                           \"--type\"\n                           \"binary\" |]\n            )\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n\n    [<Test>]\n    let ``workitem attachments download requires artifact id and output file options`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIds [| \"workitem\"\n                           \"attachments\"\n                           \"download\"\n                           Guid.NewGuid().ToString() |]\n            )\n\n        Assert.That(parseResult.Errors.Count, Is.GreaterThan(0))\n\n    [<Test>]\n    let ``workitem attachments download rejects invalid artifact id`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"workitem\"\n                                    \"attachments\"\n                                    \"download\"\n                                    \"58\"\n                                    \"--artifact-id\"\n                                    \"not-a-guid\"\n                                    \"--output-file\"\n                                    \"C:\\\\temp\\\\attachment.bin\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``workitem attachments download rejects invalid output file path`` () =\n        let parseResult =\n            GraceCommand.rootCommand.Parse(\n                withIdsAndSilent [| \"workitem\"\n                                    \"attachments\"\n                                    \"download\"\n                                    \"59\"\n                                    \"--artifact-id\"\n                                    Guid.NewGuid().ToString()\n                                    \"--output-file\"\n                                    \"C:\\\\temp\\\\invalid|name.bin\" |]\n            )\n\n        let exitCode = parseResult.Invoke()\n        exitCode |> should equal -1\n\n    [<Test>]\n    let ``workitem links remove summary parses numeric work item`` () =\n        assertParsesWithoutErrors (\n            withIds [| \"workitem\"\n                       \"links\"\n                       \"remove\"\n                       \"summary\"\n                       \"123\" |]\n        )\n\n    [<FsCheck.NUnit.Property(MaxTest = 64)>]\n    let ``workitem attach input source combinations are valid iff exactly one is selected`` (useFile: bool) (useText: bool) (useStdin: bool) =\n        let args = List<string>()\n        args.Add(\"workitem\")\n        args.Add(\"attach\")\n        args.Add(\"summary\")\n        args.Add(Guid.NewGuid().ToString())\n\n        if useFile then\n            args.Add(\"--file\")\n            args.Add(\"C:\\\\temp\\\\summary.md\")\n\n        if useText then\n            args.Add(\"--text\")\n            args.Add(\"inline summary\")\n\n        if useStdin then args.Add(\"--stdin\")\n\n        let selectedCount =\n            (if useFile then 1 else 0)\n            + (if useText then 1 else 0)\n            + (if useStdin then 1 else 0)\n\n        let parseResult =\n            args.ToArray()\n            |> withIdsAndSilent\n            |> GraceCommand.rootCommand.Parse\n\n        if selectedCount = 1 then\n            parseResult.Errors.Count = 0\n        else\n            parseResult.Invoke() = -1\n"
  },
  {
    "path": "src/Grace.Load/Grace.Load.fsproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<OutputType>Exe</OutputType>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<LangVersion>preview</LangVersion>\n\t\t<NoWarn>1057,3391</NoWarn>\n\t\t<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n\t\t<OtherFlags>--test:GraphBasedChecking</OtherFlags>\n\t\t<OtherFlags>--test:ParallelOptimization</OtherFlags>\n\t\t<OtherFlags>--test:ParallelIlxGen</OtherFlags>\n\t</PropertyGroup>\n\n\t<ItemGroup>\n\t\t<Compile Include=\"Program.Load.fs\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t  <PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n\t  <PackageReference Include=\"Spectre.Console\" Version=\"0.54.0\" />\n\t  <PackageReference Include=\"Spectre.Console.Json\" Version=\"0.54.0\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\Grace.SDK\\Grace.SDK.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Load/Program.Load.fs",
    "content": "namespace Grace\n\nopen Grace.SDK\nopen Grace.Shared\nopen Grace.Shared.Parameters\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Spectre.Console\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Linq\nopen System.Security.Cryptography\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule Load =\n\n    let numberOfRepositories = 10\n    let numberOfBranches = 100\n    let numberOfEvents = 50000\n\n    let showResult<'T> (r: GraceResult<'T>) =\n        match r with\n        | Ok result -> () //logToConsole (sprintf \"%s - CorrelationId: %s\" (result.Properties[\"EventType\"]) result.CorrelationId)\n        | Error error ->\n            if error.Exception <> ExceptionObject.Default then\n                AnsiConsole.MarkupLine($\"[Red]Error: {Markup.Escape(serialize error.Exception)}[/]\")\n            else\n                AnsiConsole.MarkupLine($\"[Red]Error: {Markup.Escape(error.Error)}[/]\")\n\n    let parallelOptions = ParallelOptions(MaxDegreeOfParallelism = Environment.ProcessorCount * 4)\n\n    [<EntryPoint>]\n    let main args =\n        (task {\n            let startTime = getCurrentInstant ()\n            let cancellationToken = new CancellationToken()\n\n            let suffixes = ConcurrentDictionary<int, string>()\n\n            for i in seq { 0 .. Math.Max(numberOfRepositories, numberOfBranches) } do\n                suffixes[i] <- Random.Shared.Next(Int32.MaxValue).ToString(\"X8\")\n\n            let repositoryIds = ConcurrentDictionary<int, RepositoryId>()\n            let parentBranchIds = ConcurrentDictionary<int, BranchId>()\n\n            let ids = ConcurrentDictionary<int, OwnerId * OrganizationId * RepositoryId * BranchId>()\n\n            let ownerId = Guid.NewGuid()\n            let ownerName = $\"Owner{suffixes[0]}\"\n            let organizationId = Guid.NewGuid()\n            let organizationName = $\"Organization{suffixes[0]}\"\n\n            match! Owner.Create(Owner.CreateOwnerParameters(OwnerId = $\"{ownerId}\", OwnerName = ownerName, CorrelationId = generateCorrelationId ())) with\n            | Ok result -> logToConsole $\"Created owner {ownerId} with OwnerName {ownerName}.\"\n            | Error error -> logToConsole $\"{error}\"\n\n            match!\n                Organization.Create\n                    (\n                        Organization.CreateOrganizationParameters(\n                            OwnerId = $\"{ownerId}\",\n                            OrganizationId = $\"{organizationId}\",\n                            OrganizationName = organizationName,\n                            CorrelationId = generateCorrelationId ()\n                        )\n                    )\n                with\n            | Ok result -> logToConsole $\"Created organization {organizationId} with OrganizationName {organizationName}.\"\n            | Error error -> logToConsole $\"{error}\"\n\n            // Warm up the /repository/create path so it's JIT-compiled and ready for\n            //   the Parallel.ForEachAsync below.\n            let warmupId = Guid.NewGuid().ToString()\n\n            let! warmupRepo =\n                Repository.Create(\n                    Repository.CreateRepositoryParameters(\n                        OwnerId = $\"{ownerId}\",\n                        OrganizationId = $\"{organizationId}\",\n                        RepositoryId = warmupId,\n                        RepositoryName = $\"Warmup{suffixes[0]}\",\n                        CorrelationId = generateCorrelationId ()\n                    )\n                )\n\n            let! deleteWarmupRepo =\n                Repository.Delete(\n                    Repository.DeleteRepositoryParameters(\n                        OwnerId = $\"{ownerId}\",\n                        OrganizationId = $\"{organizationId}\",\n                        RepositoryId = warmupId,\n                        CorrelationId = generateCorrelationId ()\n                    )\n                )\n\n            do!\n                Parallel.ForEachAsync(\n                    seq { 0 .. (numberOfRepositories - 1) },\n                    parallelOptions,\n                    (fun (i: int) (cancellationToken: CancellationToken) ->\n                        ValueTask(\n                            task {\n                                //do! Task.Delay(Random.Shared.Next(1000))\n                                let repositoryId = Guid.NewGuid()\n                                let repositoryName = $\"Repository{suffixes[i]}\"\n\n                                let! repo =\n                                    Repository.Create(\n                                        Repository.CreateRepositoryParameters(\n                                            OwnerId = $\"{ownerId}\",\n                                            OrganizationId = $\"{organizationId}\",\n                                            RepositoryId = $\"{repositoryId}\",\n                                            RepositoryName = repositoryName,\n                                            ObjectStorageProvider = ObjectStorageProvider.DefaultObjectStorageProvider,\n                                            CorrelationId = generateCorrelationId ()\n                                        )\n                                    )\n\n                                repositoryIds.AddOrUpdate(i, repositoryId, (fun _ _ -> repositoryId))\n                                |> ignore\n\n                                match repo with\n                                | Ok r ->\n                                    logToConsole $\"Added repository {i}; repositoryId: {repositoryId}; repositoryName: {repositoryName}.\"\n\n                                    let! rrrrr =\n                                        Repository.SetLogicalDeleteDays(\n                                            Repository.SetLogicalDeleteDaysParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                LogicalDeleteDays = single (TimeSpan.FromSeconds(90.0).TotalDays),\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    let! rrrrr =\n                                        Repository.SetSaveDays(\n                                            Repository.SetSaveDaysParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                SaveDays = single (TimeSpan.FromSeconds(90.0).TotalDays),\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    let! rrrrr =\n                                        Repository.SetCheckpointDays(\n                                            Repository.SetCheckpointDaysParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                CheckpointDays = single (TimeSpan.FromSeconds(90.0).TotalDays),\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    match!\n                                        Branch.Get\n                                            (\n                                                Branch.GetBranchParameters(\n                                                    OwnerId = $\"{ownerId}\",\n                                                    OrganizationId = $\"{organizationId}\",\n                                                    RepositoryId = $\"{repositoryId}\",\n                                                    BranchName = Constants.InitialBranchName,\n                                                    CorrelationId = generateCorrelationId ()\n                                                )\n                                            )\n                                        with\n                                    | Ok mainBranch ->\n                                        logToConsole $\"Adding parentBranchId {i}; mainBranch.ReturnValue.BranchId: {mainBranch.ReturnValue.BranchId}.\"\n\n                                        parentBranchIds.AddOrUpdate(i, mainBranch.ReturnValue.BranchId, (fun _ _ -> mainBranch.ReturnValue.BranchId))\n                                        |> ignore\n                                    | Error error -> logToConsole $\"Error getting main: {error}\"\n                                | Error error -> logToConsole $\"Error creating repository: {error}\"\n\n                                showResult repo\n                            }\n                        ))\n                )\n\n            do!\n                Parallel.ForEachAsync(\n                    seq { 0 .. (numberOfBranches - 1) },\n                    parallelOptions,\n                    (fun (i: int) (cancellationToken: CancellationToken) ->\n                        ValueTask(\n                            task {\n                                //do! Task.Delay(Random.Shared.Next(1000))\n                                let branchId = Guid.NewGuid()\n                                let branchName = $\"Branch{suffixes[i]}\"\n                                let repositoryIndex = Random.Shared.Next(repositoryIds.Count)\n                                let repositoryId = repositoryIds[repositoryIndex]\n                                let parentBranchId = parentBranchIds[repositoryIndex]\n\n                                try\n                                    let! r =\n                                        Branch.Create(\n                                            Branch.CreateBranchParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                BranchName = branchName,\n                                                ParentBranchId = $\"{parentBranchId}\",\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n\n                                    match r with\n                                    | Ok r ->\n                                        ids.AddOrUpdate(\n                                            i,\n                                            (ownerId, organizationId, repositoryId, branchId),\n                                            (fun _ _ -> (ownerId, organizationId, repositoryId, branchId))\n                                        )\n                                        |> ignore\n\n                                        logToConsole $\"Added to ids: {(ownerId, organizationId, repositoryId, branchId)}; Count: {ids.Count}.\"\n                                    | Error error -> logToConsole $\"Error creating branch {i}: {error}.\"\n                                with\n                                | ex -> logToConsole $\"i: {i}; exception; repositoryId: {repositoryId}; parentBranchId: {parentBranchId}.\"\n                            }\n                        ))\n                )\n\n            let setupTime = getCurrentInstant ()\n            logToConsole $\"Setup complete. numberOfRepositories: {numberOfRepositories}; numberOfBranches: {numberOfBranches}; ids.Count: {ids.Count}.\"\n            logToConsole $\"-----------------\"\n\n            let mutable chunkStartInstant = getCurrentInstant ()\n            let chunkTransactionsPerSecond = List<float>()\n\n            do!\n                Parallel.ForEachAsync(\n                    seq { 1..numberOfEvents },\n                    parallelOptions,\n                    (fun (i: int) (cancellationToken: CancellationToken) ->\n                        ValueTask(\n                            task {\n                                if i % 250 = 0 then\n                                    let chunkTPS =\n                                        float 250\n                                        / (getCurrentInstant () - chunkStartInstant)\n                                            .TotalSeconds\n\n                                    chunkTransactionsPerSecond.Add(chunkTPS)\n\n                                    let rollingAverage = chunkTransactionsPerSecond.TakeLast(20).Average()\n\n                                    let totalTPS =\n                                        float i\n                                        / (getCurrentInstant () - setupTime).TotalSeconds\n\n                                    let threads = $\"ThreadCount: {ThreadPool.ThreadCount}; PendingWorkItemCount: {ThreadPool.PendingWorkItemCount}\"\n\n                                    logToConsole\n                                        $\"Processing event {i} of {numberOfEvents}; Chunk transactions/sec: {chunkTPS:F3}; Rolling average (previous 20): {rollingAverage:F3}; Total transactions/sec: {totalTPS:F3}; Thread counts: {threads}.\"\n\n                                    chunkStartInstant <- getCurrentInstant ()\n\n                                let rnd = Random.Shared.Next(ids.Count)\n                                let (ownerId, organizationId, repositoryId, branchId) = ids[rnd]\n                                let sha256 = SHA256.Create()\n                                let byteArray = Array.init 64 (fun _ -> byte (Random.Shared.Next(256)))\n                                let sha256Hash = byteArrayToString (sha256.ComputeHash(byteArray).AsSpan())\n\n                                match Random.Shared.Next(0, 13) with\n                                | 0 ->\n                                    let! r =\n                                        Branch.Save(\n                                            Branch.CreateReferenceParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                Message = $\"Save - {DateTime.UtcNow}\",\n                                                DirectoryVersionId = Guid.NewGuid(),\n                                                Sha256Hash = sha256Hash,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 1 ->\n                                    let! r =\n                                        Branch.Checkpoint(\n                                            Branch.CreateReferenceParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                Message = $\"Checkpoint - {DateTime.UtcNow}\",\n                                                DirectoryVersionId = Guid.NewGuid(),\n                                                Sha256Hash = sha256Hash,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 2 ->\n                                    let! r =\n                                        Branch.Commit(\n                                            Branch.CreateReferenceParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                Message = $\"Commit - {DateTime.UtcNow}\",\n                                                DirectoryVersionId = Guid.NewGuid(),\n                                                Sha256Hash = sha256Hash,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 3 ->\n                                    let! r =\n                                        Branch.Tag(\n                                            Branch.CreateReferenceParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                Message = $\"Tag - {DateTime.UtcNow}\",\n                                                DirectoryVersionId = Guid.NewGuid(),\n                                                Sha256Hash = sha256Hash,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 4 ->\n                                    let! r =\n                                        Branch.Get(\n                                            Branch.GetBranchParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 5 ->\n                                    let! r =\n                                        Branch.GetReferences(\n                                            Branch.GetReferencesParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                MaxCount = 30,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 6 ->\n                                    let! r =\n                                        Branch.GetCommits(\n                                            Branch.GetReferencesParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                MaxCount = 30,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 7 ->\n                                    let! r =\n                                        Branch.GetTags(\n                                            Branch.GetReferencesParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                MaxCount = 30,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 8 ->\n                                    let! r =\n                                        Repository.Get(\n                                            Repository.GetBranchesParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 9 ->\n                                    let! r =\n                                        Repository.Get(\n                                            Repository.GetRepositoryParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 10 ->\n                                    let! r =\n                                        Repository.GetBranchesByBranchId(\n                                            Repository.GetBranchesByBranchIdParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchIds = [| branchId |],\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 11 ->\n                                    let! r =\n                                        Branch.GetCheckpoints(\n                                            Branch.GetReferencesParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                MaxCount = 30,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | 12 ->\n                                    let! r =\n                                        Branch.GetSaves(\n                                            Branch.GetReferencesParameters(\n                                                OwnerId = $\"{ownerId}\",\n                                                OrganizationId = $\"{organizationId}\",\n                                                RepositoryId = $\"{repositoryId}\",\n                                                BranchId = $\"{branchId}\",\n                                                MaxCount = 30,\n                                                CorrelationId = generateCorrelationId ()\n                                            )\n                                        )\n\n                                    showResult r\n                                | _ -> ()\n                            }\n                        ))\n                )\n\n            let mainProcessingTime = getCurrentInstant ()\n            logToConsole \"Main processing complete.\"\n            logToConsole $\"Processing time:  {mainProcessingTime - setupTime}.\"\n\n            logToConsole\n                $\"Transactions/sec: {float numberOfEvents\n                                     / (mainProcessingTime - setupTime).TotalSeconds}\"\n\n            logToConsole \"Starting tear down.\"\n\n            // Tear down; delete branches and repositories, then delete organization and owner.\n\n            // Setting MaxDegreeOfParallelism to 4 to avoid 429 Too Many Requests errors.\n            let deleteParallelOptions = ParallelOptions(MaxDegreeOfParallelism = 4)\n            let mutable deleteCount = 0\n\n            // Delete branches.\n            do!\n                Parallel.ForEachAsync(\n                    ids.Values,\n                    deleteParallelOptions,\n                    (fun id (cancellationToken: CancellationToken) ->\n                        ValueTask(\n                            task {\n                                let (ownerId, organizationId, repositoryId, branchId) = id\n\n                                let! result =\n                                    Branch.Delete(\n                                        Branch.DeleteBranchParameters(\n                                            OwnerId = $\"{ownerId}\",\n                                            OrganizationId = $\"{organizationId}\",\n                                            RepositoryId = $\"{repositoryId}\",\n                                            BranchId = $\"{branchId}\",\n                                            CorrelationId = generateCorrelationId ()\n                                        )\n                                    )\n\n                                showResult result\n                                let deleteCount = Interlocked.Increment(&deleteCount)\n\n                                if deleteCount % 100 = 0 then\n                                    logToConsole $\"Deleted {deleteCount} of {ids.Count} branches.\"\n                            //do! Task.Delay(Random.Shared.Next(100))\n                            }\n                        ))\n                )\n\n            deleteCount <- 0\n\n            // Delete repositories.\n            do!\n                Parallel.ForEachAsync(\n                    ids\n                        .Values\n                        .Select(fun (ownerId, organizationId, repositoryId, branchId) -> (ownerId, organizationId, repositoryId))\n                        .Distinct(),\n                    deleteParallelOptions,\n                    (fun id (cancellationToken: CancellationToken) ->\n                        ValueTask(\n                            task {\n                                let (ownerId, organizationId, repositoryId) = id\n\n                                let! result =\n                                    Repository.Delete(\n                                        Repository.DeleteRepositoryParameters(\n                                            OwnerId = $\"{ownerId}\",\n                                            OrganizationId = $\"{organizationId}\",\n                                            RepositoryId = $\"{repositoryId}\",\n                                            DeleteReason = \"performance test\",\n                                            CorrelationId = generateCorrelationId ()\n                                        )\n                                    )\n\n                                showResult result\n                                let deleteCount = Interlocked.Increment(&deleteCount)\n\n                                if deleteCount % 25 = 0 then\n                                    logToConsole $\"Deleted {deleteCount} of {numberOfRepositories} repositories.\"\n\n                            //do! Task.Delay(Random.Shared.Next(50))\n                            }\n                        ))\n                )\n\n            logToConsole $\"Deleted {deleteCount} of {numberOfRepositories} repositories.\"\n\n            // Delete organization.\n            let! r =\n                Organization.Delete(\n                    Organization.DeleteOrganizationParameters(\n                        OwnerId = $\"{ownerId}\",\n                        OrganizationId = $\"{organizationId}\",\n                        DeleteReason = \"performance test\",\n                        CorrelationId = generateCorrelationId ()\n                    )\n                )\n\n            showResult r\n\n            // Delete owner.\n            let! r =\n                Owner.Delete(Owner.DeleteOwnerParameters(OwnerId = $\"{ownerId}\", DeleteReason = \"performance test\", CorrelationId = generateCorrelationId ()))\n\n            showResult r\n\n            // Wrap up.\n            let endTime = getCurrentInstant ()\n            logToConsole \"Tear down complete.\"\n\n            printfn $\"Number of events: {numberOfEvents}.\"\n            printfn $\"Setup time:       {setupTime - startTime}.\"\n            printfn $\"Processing time:  {mainProcessingTime - setupTime}.\"\n            printfn $\"Tear down:        {endTime - mainProcessingTime}.\"\n            printfn $\"Total:            {endTime - startTime}.\"\n\n            printfn\n                $\"Transactions/sec: {float numberOfEvents\n                                     / (mainProcessingTime - setupTime).TotalSeconds}\"\n\n            return 0\n        })\n            .Result\n"
  },
  {
    "path": "src/Grace.Load/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"WSL\": {\n      \"commandName\": \"WSL2\",\n      \"distributionName\": \"\"\n    },\n    \"graceload\": {\n      \"commandName\": \"Project\",\n      \"workingDirectory\": \"D:\\\\Source\\\\Grace\\\\src\"\n    }\n  }\n}"
  },
  {
    "path": "src/Grace.Orleans.CodeGen/Declaration.Orleans.CodeGen.cs",
    "content": "// The GenerateCodeForDeclaringAssembly attribute finds all .NET types in the assembly that contains the type you specify -\n//   you can pick any type in the assembly, it doesn't matter - and submits them to Orleans code-generation to create\n//   serialization code for them. Weird, but it works.\n\n// This line gets everything from Grace.Types.\n[assembly: GenerateCodeForDeclaringAssembly(typeof(Grace.Types.Types.OwnerType))]\n\n// This line gets everything from Grace.Actors.\n[assembly: GenerateCodeForDeclaringAssembly(typeof(Grace.Actors.Owner.OwnerActor))]\n"
  },
  {
    "path": "src/Grace.Orleans.CodeGen/Grace.Orleans.CodeGen.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<ImplicitUsings>enable</ImplicitUsings>\n\t\t<Nullable>enable</Nullable>\n\t\t<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>\n        <PublishReadyToRun>true</PublishReadyToRun>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n        <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <NoWarn>1591</NoWarn>\n    </PropertyGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Microsoft.Orleans.Sdk\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Serialization.FSharp\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Serialization.SystemTextJson\" Version=\"9.2.1\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n        <ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n        <ProjectReference Include=\"..\\Grace.Actors\\Grace.Actors.fsproj\" />\n    </ItemGroup>\n\t\n</Project>\n"
  },
  {
    "path": "src/Grace.Orleans.CodeGen/instructions.md",
    "content": "﻿# Grace.Orleans.CodeGen — instructions.md\n\nSee repository-wide guidance in `../agents.md`. The sections below are project-specific patterns and rules.\n\n## Purpose\nContains assembly-level declarations to trigger Orleans code generation for serialization.\n\n## Key patterns\n- Minimal C# files with `[assembly: ...]` attributes referencing domain types.\n\n## Project rules for agents\n1. Keep this project minimal. When adding types, reference a type in the declaring assembly and explain why in a comment.\n2. Avoid changing assembly names or project structure here.\n"
  },
  {
    "path": "src/Grace.SDK/AGENTS.md",
    "content": "# Grace.SDK Agents Guide\n\nConsult `../AGENTS.md` for global policies before modifying the SDK.\n\n## Purpose\n\n- Offer stable client-side APIs used by the CLI and other consumers to\n  interact with Grace services and actors.\n- Provide thin wrappers around server endpoints and actor proxies while\n  keeping DTOs aligned with shared domain types.\n\n## Key Patterns\n\n- DTOs are records with descriptive field names that map directly to\n  `Grace.Types` definitions -- sync changes across both projects.\n- Wrap HTTP calls and actor interactions in lightweight modules; avoid\n  embedding heavy business logic inside transport layers.\n- Favor additive API expansions or versioned members when evolution is\n  required; deprecate before removing public surface area.\n- Document notable auth, retry, or transport assumptions here to spare\n  agents from scanning implementation details.\n\n## Project Rules\n\n1. Maintain public API stability; breaking changes require coordination\n   with CLI and external clients plus versioned alternatives.\n2. Align SDK method contracts, routes, and DTOs with the server; update\n   integration tests or samples when they diverge.\n3. Record any new surface area, release notes, or migration steps inside\n   this file to keep future agents informed.\n\n## Validation\n\n- Cover new behaviors with unit tests that mock transport layers, actor\n  proxies, or HTTP abstractions.\n- Execute `dotnet build --configuration Release` and targeted tests\n  before shipping.\n- When changing public APIs, update consumer samples and note doc\n  updates required during PR review.\n\n## Notes\n\n- Added SDK modules for WorkItem, Policy, Review, Queue, and PromotionSet\n  APIs (January 6, 2026).\n- Queue SDK adds pause/resume/dequeue helpers aligned to server routes\n  (January 6, 2026).\n- Added `Auth.getOidcClientConfig` for fetching server-provided OIDC client\n  configuration (January 8, 2026).\n"
  },
  {
    "path": "src/Grace.SDK/Access.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Access\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Authorization\nopen Grace.Types.Types\n\ntype Access() =\n\n    /// Grants a role to a principal at a scope.\n    static member public GrantRole(parameters: GrantRoleParameters) =\n        postServer<GrantRoleParameters, RoleAssignment list> (parameters |> ensureCorrelationIdIsSet, $\"access/{nameof (Access.GrantRole)}\")\n\n    /// Revokes a role from a principal at a scope.\n    static member public RevokeRole(parameters: RevokeRoleParameters) =\n        postServer<RevokeRoleParameters, RoleAssignment list> (parameters |> ensureCorrelationIdIsSet, $\"access/{nameof (Access.RevokeRole)}\")\n\n    /// Lists role assignments at a scope, optionally filtered by principal.\n    static member public ListRoleAssignments(parameters: ListRoleAssignmentsParameters) =\n        postServer<ListRoleAssignmentsParameters, RoleAssignment list> (parameters |> ensureCorrelationIdIsSet, $\"access/{nameof (Access.ListRoleAssignments)}\")\n\n    /// Upserts a repository path permission.\n    static member public UpsertPathPermission(parameters: UpsertPathPermissionParameters) =\n        postServer<UpsertPathPermissionParameters, PathPermission list> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"access/{nameof (Access.UpsertPathPermission)}\"\n        )\n\n    /// Removes a repository path permission.\n    static member public RemovePathPermission(parameters: RemovePathPermissionParameters) =\n        postServer<RemovePathPermissionParameters, PathPermission list> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"access/{nameof (Access.RemovePathPermission)}\"\n        )\n\n    /// Lists repository path permissions.\n    static member public ListPathPermissions(parameters: ListPathPermissionsParameters) =\n        postServer<ListPathPermissionsParameters, PathPermission list> (parameters |> ensureCorrelationIdIsSet, $\"access/{nameof (Access.ListPathPermissions)}\")\n\n    /// Checks a permission against the current or specified principal.\n    static member public CheckPermission(parameters: CheckPermissionParameters) =\n        postServer<CheckPermissionParameters, PermissionCheckResult> (parameters |> ensureCorrelationIdIsSet, $\"access/{nameof (Access.CheckPermission)}\")\n\n    /// Lists the configured roles.\n    static member public ListRoles(parameters: CommonParameters) =\n        getServer<CommonParameters, RoleDefinition list> (parameters |> ensureCorrelationIdIsSet, \"access/listRoles\")\n"
  },
  {
    "path": "src/Grace.SDK/Admin.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Reminder\nopen Grace.Shared.Utilities\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule Admin =\n\n    type Reminder() =\n\n        /// <summary>\n        /// Lists reminders for a repository with optional filters.\n        /// </summary>\n        /// <param name=\"parameters\">Values to use when listing reminders.</param>\n        static member public List(parameters: ListRemindersParameters) =\n            postServer<ListRemindersParameters, IEnumerable<ReminderDto>> (parameters |> ensureCorrelationIdIsSet, $\"reminder/{nameof (Reminder.List)}\")\n\n        /// <summary>\n        /// Gets a specific reminder by its ID.\n        /// </summary>\n        /// <param name=\"parameters\">Values to use when getting the reminder.</param>\n        static member public Get(parameters: GetReminderParameters) =\n            postServer<GetReminderParameters, ReminderDto> (parameters |> ensureCorrelationIdIsSet, $\"reminder/{nameof (Reminder.Get)}\")\n\n        /// <summary>\n        /// Deletes a reminder.\n        /// </summary>\n        /// <param name=\"parameters\">Values to use when deleting the reminder.</param>\n        static member public Delete(parameters: DeleteReminderParameters) =\n            postServer<DeleteReminderParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"reminder/{nameof (Reminder.Delete)}\")\n\n        /// <summary>\n        /// Updates the fire time for a reminder.\n        /// </summary>\n        /// <param name=\"parameters\">Values to use when updating the reminder.</param>\n        static member public UpdateTime(parameters: UpdateReminderTimeParameters) =\n            postServer<UpdateReminderTimeParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"reminder/{nameof (Reminder.UpdateTime)}\")\n\n        /// <summary>\n        /// Reschedules a reminder relative to now.\n        /// </summary>\n        /// <param name=\"parameters\">Values to use when rescheduling the reminder.</param>\n        static member public Reschedule(parameters: RescheduleReminderParameters) =\n            postServer<RescheduleReminderParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"reminder/{nameof (Reminder.Reschedule)}\")\n\n        /// <summary>\n        /// Creates a new manual reminder.\n        /// </summary>\n        /// <param name=\"parameters\">Values to use when creating the reminder.</param>\n        static member public Create(parameters: CreateReminderParameters) =\n            postServer<CreateReminderParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"reminder/{nameof (Reminder.Create)}\")\n"
  },
  {
    "path": "src/Grace.SDK/Artifact.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Artifact\nopen Grace.Types.Artifact\nopen System\n\n/// Client API for artifact endpoints.\ntype Artifact() =\n\n    static member internal BuildDownloadUriRoute(parameters: GetArtifactDownloadUriParameters) =\n        let artifactId = Uri.EscapeDataString(parameters.ArtifactId.Trim())\n        let organizationId = Uri.EscapeDataString(parameters.OrganizationId.Trim())\n        let repositoryId = Uri.EscapeDataString(parameters.RepositoryId.Trim())\n        let correlationId = Uri.EscapeDataString(parameters.CorrelationId.Trim())\n\n        $\"artifact/{artifactId}/download-uri?organizationId={organizationId}&repositoryId={repositoryId}&correlationId={correlationId}\"\n\n    /// Creates artifact metadata and returns an upload URI.\n    static member public Create(parameters: CreateArtifactParameters) =\n        postServer<CreateArtifactParameters, ArtifactCreateResult> (parameters |> ensureCorrelationIdIsSet, \"artifact/create\")\n\n    /// Gets a read URI for an existing artifact.\n    static member public GetDownloadUri(parameters: GetArtifactDownloadUriParameters) =\n        let normalizedParameters = parameters |> ensureCorrelationIdIsSet\n        let route = Artifact.BuildDownloadUriRoute normalizedParameters\n\n        getServer<GetArtifactDownloadUriParameters, ArtifactDownloadUriResult> (normalizedParameters, route)\n"
  },
  {
    "path": "src/Grace.SDK/Auth.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.Shared\nopen Grace.Shared.Services\nopen Grace.Shared.Utilities\nopen Grace.Types.Auth\nopen Grace.Types.Types\nopen System\nopen System.Net.Http\nopen System.Net.Http.Headers\nopen System.Net.Http.Json\nopen System.Threading.Tasks\n\nmodule Auth =\n\n    type TokenProvider = unit -> Task<string option>\n\n    let mutable private tokenProvider: TokenProvider option = None\n\n    let setTokenProvider (provider: TokenProvider) = tokenProvider <- Some provider\n\n    let clearTokenProvider () = tokenProvider <- None\n\n    let tryGetAccessToken () =\n        task {\n            match tokenProvider with\n            | None -> return None\n            | Some provider ->\n                try\n                    return! provider ()\n                with\n                | _ -> return None\n        }\n\n    let addAuthorizationHeader (httpClient: HttpClient) =\n        task {\n            let! tokenOpt = tryGetAccessToken ()\n\n            match tokenOpt with\n            | Some token when not (String.IsNullOrWhiteSpace token) ->\n                httpClient.DefaultRequestHeaders.Authorization <- AuthenticationHeaderValue(\"Bearer\", token)\n            | _ -> ()\n        }\n\n    let getOidcClientConfig (parameters: Grace.Shared.Parameters.Common.CommonParameters) =\n        task {\n            let correlationId = ensureNonEmptyCorrelationId parameters.CorrelationId\n            parameters.CorrelationId <- correlationId\n\n            try\n                use httpClient = getHttpClient correlationId\n                let startTime = getCurrentInstant ()\n\n                let graceServerUri = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GraceServerUri)\n                let serverUri = Uri($\"{graceServerUri}/auth/oidc/config\")\n\n                let! response = Constants.DefaultAsyncRetryPolicy.ExecuteAsync(fun _ -> httpClient.GetAsync(serverUri))\n                let endTime = getCurrentInstant ()\n\n                if response.IsSuccessStatusCode then\n                    let! graceReturnValue = response.Content.ReadFromJsonAsync<GraceReturnValue<OidcClientConfig>>(Constants.JsonSerializerOptions)\n\n                    return\n                        Ok graceReturnValue\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                else\n                    let! graceError = response.Content.ReadFromJsonAsync<GraceError>(Constants.JsonSerializerOptions)\n\n                    return\n                        Error graceError\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n            with\n            | ex ->\n                let exceptionResponse = Utilities.ExceptionResponse.Create ex\n                return Error(GraceError.Create (serialize exceptionResponse) parameters.CorrelationId)\n        }\n"
  },
  {
    "path": "src/Grace.SDK/Branch.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Branch\nopen Grace.Shared.Utilities\nopen Grace.Types.Branch\nopen Grace.Types.Diff\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\nopen Grace.Types.DirectoryVersion\n\n/// The Branch module provides a set of functions for interacting with branches in the Grace API.\ntype Branch() =\n\n    /// Creates a new branch.\n    static member public Create(parameters: CreateBranchParameters) =\n        postServer<CreateBranchParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Create)}\")\n\n    /// Rebases a branch on a promotion from the parent branch.\n    static member public Rebase(parameters: RebaseParameters) =\n        postServer<RebaseParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Rebase)}\")\n\n    /// Assigns a specific version of the repository to be the next promotion in a branch.\n    static member public Assign(parameters: AssignParameters) =\n        postServer<AssignParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Assign)}\")\n\n    /// Creates a promotion reference in the parent branch of this branch.\n    static member public Promote(parameters: CreateReferenceParameters) =\n        postServer<CreateReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Promote)}\")\n\n    /// Creates a commit reference in this branch.\n    static member public Commit(parameters: CreateReferenceParameters) =\n        postServer<CreateReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Commit)}\")\n\n    /// Creates a checkpoint reference in this branch.\n    static member public Checkpoint(parameters: CreateReferenceParameters) =\n        postServer<CreateReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Checkpoint)}\")\n\n    /// Creates a save reference in this branch.\n    static member public Save(parameters: CreateReferenceParameters) =\n        postServer<CreateReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Save)}\")\n\n    /// Creates a tag reference in this branch.\n    static member public Tag(parameters: CreateReferenceParameters) =\n        postServer<CreateReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Tag)}\")\n\n    /// Creates an external reference in this branch.\n    static member public CreateExternal(parameters: CreateReferenceParameters) =\n        postServer<CreateReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.CreateExternal)}\")\n\n    /// Sets the flag to allow `grace assign` in this branch.\n    static member public EnableAssign(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableAssign)}\")\n\n    /// Sets the flag to allow promotion in this branch.\n    static member public EnablePromotion(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnablePromotion)}\")\n\n    /// Sets the flag to allow commits in this branch.\n    static member public EnableCommit(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableCommit)}\")\n\n    /// Sets the flag to allow checkpoints in this branch.\n    static member public EnableCheckpoint(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableCheckpoint)}\")\n\n    /// Sets the flag to allow saves in this branch.\n    static member public EnableSave(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableSave)}\")\n\n    /// Sets the flag to allow tags in this branch.\n    static member public EnableTag(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableTag)}\")\n\n    /// Sets the flag to allow external references in this branch.\n    static member public EnableExternal(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableExternal)}\")\n\n    /// Sets the flag to allow auto-rebase in this branch.\n    static member public EnableAutoRebase(parameters: EnableFeatureParameters) =\n        postServer<EnableFeatureParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.EnableAutoRebase)}\")\n\n    /// Gets the diffs between a set of references.\n    static member public GetDiffsForReferenceType(parameters: GetDiffsForReferenceTypeParameters) =\n        postServer<GetDiffsForReferenceTypeParameters, (IReadOnlyList<ReferenceDto> * IReadOnlyList<DiffDto>)> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"branch/{nameof (Branch.GetDiffsForReferenceType)}\"\n        )\n\n    /// Gets the metadata for a specific reference from a branch.\n    static member public GetReference(parameters: GetReferenceParameters) =\n        postServer<GetReferenceParameters, ReferenceDto> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetReference)}\")\n\n    /// Gets the references from a branch.\n    static member public GetReferences(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetReferences)}\")\n\n    /// Gets the promotions from a branch.\n    static member public GetPromotions(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetPromotions)}\")\n\n    /// Gets the commits from a branch.\n    static member public GetCommits(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetCommits)}\")\n\n    /// Gets the checkpoints from a branch.\n    static member public GetCheckpoints(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetCheckpoints)}\")\n\n    /// Gets the saves from a branch.\n    static member public GetSaves(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetSaves)}\")\n\n    /// Gets the tags from a branch.\n    static member public GetTags(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetTags)}\")\n\n    /// Gets the external references from a branch.\n    static member public GetExternals(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetExternals)}\")\n\n    /// Gets the rebases from a branch.\n    static member public GetRebases(parameters: GetReferencesParameters) =\n        postServer<GetReferencesParameters, ReferenceDto array> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetRebases)}\")\n\n    /// Sets the name of a branch.\n    static member public SetName(parameters: SetBranchNameParameters) =\n        postServer<SetBranchNameParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.SetName)}\")\n\n    /// Gets the metadata for a branch.\n    static member public Get(parameters: GetBranchParameters) =\n        postServer<GetBranchParameters, BranchDto> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Get)}\")\n\n    /// Gets the events handled by a branch.\n    static member public GetEvents(parameters: GetBranchVersionParameters) =\n        postServer<GetBranchVersionParameters, IEnumerable<string>> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetEvents)}\")\n\n    /// Gets the parent branch of the branch specified in the parameters.\n    static member public GetParentBranch(parameters: BranchParameters) =\n        postServer<BranchParameters, BranchDto> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetParentBranch)}\")\n\n    /// Gets a specific version of a branch from the server.\n    static member public GetVersion(parameters: GetBranchVersionParameters) =\n        postServer<GetBranchVersionParameters, IEnumerable<DirectoryVersionId>> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetVersion)}\")\n\n    /// Gets the DirectoryVersions for a specific version of a branch from the server.\n    static member public ListContents(parameters: ListContentsParameters) =\n        postServer<ListContentsParameters, IEnumerable<DirectoryVersionDto>> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.ListContents)}\")\n\n    /// Gets the recursive size of a branch.\n    static member public GetRecursiveSize(parameters: ListContentsParameters) =\n        postServer<ListContentsParameters, int64> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.GetRecursiveSize)}\")\n\n    /// Delete the branch.\n    static member public Delete(parameters: DeleteBranchParameters) =\n        postServer<DeleteBranchParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.Delete)}\")\n\n    /// Update the parent branch of a branch.\n    static member public UpdateParentBranch(parameters: UpdateParentBranchParameters) =\n        postServer<UpdateParentBranchParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.UpdateParentBranch)}\")\n\n    /// Sets the promotion mode for a branch.\n    static member public SetPromotionMode(parameters: SetPromotionModeParameters) =\n        postServer<SetPromotionModeParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"branch/{nameof (Branch.SetPromotionMode)}\")\n"
  },
  {
    "path": "src/Grace.SDK/Common.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Services\nopen Grace.Types.Automation\nopen Grace.Types.Types\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation\nopen NodaTime\nopen Polly\nopen System\nopen System.Diagnostics\nopen System.IO\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\nopen System.Net.Security\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\nopen Microsoft.Extensions.Caching.Memory\n\n// Supresses the warning for using AllowNoEncryption in Debug builds.\n#nowarn \"0044\"\n\nmodule Common =\n\n    /// <summary>\n    /// Sends GET commands to Grace Server.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when sending the GET command.</param>\n    /// <param name=\"route\">The route to use when sending the GET command.</param>\n    /// <returns>A Task containing the result of the GET command.</returns>\n    /// <typeparam name=\"'T\">The type of the parameters to use when sending the GET command.</typeparam>\n    /// <typeparam name=\"'U\">The type of the result of the command.</typeparam>\n    let getServer<'T, 'U when 'T :> CommonParameters> (parameters: 'T, route: string) =\n        task {\n            try\n                //checkMemoryCache ()\n                use httpClient = getHttpClient parameters.CorrelationId\n                do! Auth.addAuthorizationHeader httpClient\n                let startTime = getCurrentInstant ()\n\n                let graceServerUri = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GraceServerUri)\n\n                let serverUri = Uri($\"{graceServerUri}/{route}\")\n\n                let! response = Constants.DefaultAsyncRetryPolicy.ExecuteAsync(fun _ -> httpClient.GetAsync(new Uri($\"{serverUri}\")))\n\n                let endTime = getCurrentInstant ()\n\n                if response.IsSuccessStatusCode then\n                    let! graceReturn = response.Content.ReadFromJsonAsync<GraceReturnValue<'U>>(Constants.JsonSerializerOptions)\n\n                    return\n                        Ok graceReturn\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                else\n                    let! graceError = response.Content.ReadFromJsonAsync<GraceError>(Constants.JsonSerializerOptions)\n\n                    return\n                        Error graceError\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n            with\n            | ex ->\n                let exceptionResponse = Utilities.ExceptionResponse.Create ex\n                return Error(GraceError.Create (serialize exceptionResponse) parameters.CorrelationId)\n        }\n\n    /// <summary>\n    /// Sends POST commands to Grace Server.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when sending the POST command.</param>\n    /// <param name=\"route\">The route to use when sending the POST command.</param>\n    /// <returns>A Task containing the result of the POST command.</returns>\n    /// <typeparam name=\"'T\">The type of the parameters to use when sending the POST command.</typeparam>\n    /// <typeparam name=\"'U\">The type of the result of the command.</typeparam>\n    let postServer<'T, 'U when 'T :> CommonParameters> (parameters: 'T, route: string) : (Task<GraceResult<'U>>) =\n        task {\n            try\n                //checkMemoryCache ()\n                use httpClient = getHttpClient parameters.CorrelationId\n                do! Auth.addAuthorizationHeader httpClient\n                let serverUriWithRoute = Uri($\"{Current().ServerUri}/{route}\")\n                let startTime = getCurrentInstant ()\n                let! response = httpClient.PostAsync(serverUriWithRoute, createJsonContent parameters)\n\n                let endTime = getCurrentInstant ()\n\n                if response.IsSuccessStatusCode then\n                    let! graceReturnValue = response.Content.ReadFromJsonAsync<GraceReturnValue<'U>>(Constants.JsonSerializerOptions)\n                    //let! blah = response.Content.ReadAsStringAsync()\n                    //logToConsole $\"blah: {blah}\"\n                    //let graceReturnValue = JsonSerializer.Deserialize<GraceReturnValue<'U>>(blah, Constants.JsonSerializerOptions)\n\n                    return\n                        Ok graceReturnValue\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                else if response.StatusCode = HttpStatusCode.NotFound then\n                    return Error(GraceError.Create $\"Server endpoint {route} not found.\" parameters.CorrelationId)\n                elif response.StatusCode = HttpStatusCode.BadRequest then\n                    let! errorMessage = response.Content.ReadAsStringAsync()\n\n                    return\n                        Error(GraceError.Create $\"{errorMessage}\" parameters.CorrelationId)\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                        |> enhance \"StatusCode\" $\"{response.StatusCode}\"\n                else\n                    let! responseAsString = response.Content.ReadAsStringAsync()\n\n                    try\n                        let graceError = deserialize<GraceError> (responseAsString)\n\n                        return\n                            Error graceError\n                            |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                            |> enhance \"StatusCode\" $\"{response.StatusCode}\"\n                    with\n                    | ex ->\n                        return\n                            Error(GraceError.Create $\"{responseAsString}\" parameters.CorrelationId)\n                            |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                            |> enhance \"StatusCode\" $\"{response.StatusCode}\"\n            with\n            | ex ->\n                let exceptionResponse = Utilities.ExceptionResponse.Create ex\n                return Error(GraceError.Create ($\"{exceptionResponse}\") parameters.CorrelationId)\n        }\n\n    /// Ensures that the CorrelationId is set in the parameters for calling Grace Server. If it hasn't already been set, one will be created.\n    let ensureCorrelationIdIsSet<'T when 'T :> CommonParameters> (parameters: 'T) =\n        parameters.CorrelationId <- (Utilities.ensureNonEmptyCorrelationId parameters.CorrelationId)\n        parameters\n\n    /// Returns the object file name for a given file version, including the SHA-256 hash.\n    ///\n    /// Example: foo.txt with a SHA-256 hash of \"8e798...980c\" -> \"foo_8e798...980c.txt\".\n    let getObjectFileName (fileVersion: FileVersion) =\n        let file = FileInfo(Path.Combine(Current().RootDirectory, $\"{fileVersion.RelativePath}\"))\n\n        $\"{file.Name.Replace(file.Extension, String.Empty)}_{fileVersion.Sha256Hash}{file.Extension}\"\n\n/// Agent session lifecycle API methods.\ntype AgentSession() =\n    /// Starts an agent session for a work item.\n    static member public Start(parameters: StartAgentSessionParameters) =\n        Common.postServer<StartAgentSessionParameters, AgentSessionOperationResult> (\n            parameters |> Common.ensureCorrelationIdIsSet,\n            \"agent/session/start\"\n        )\n\n    /// Stops an active agent session.\n    static member public Stop(parameters: StopAgentSessionParameters) =\n        Common.postServer<StopAgentSessionParameters, AgentSessionOperationResult> (\n            parameters |> Common.ensureCorrelationIdIsSet,\n            \"agent/session/stop\"\n        )\n\n    /// Gets status for a specific or current agent session.\n    static member public Status(parameters: GetAgentSessionStatusParameters) =\n        Common.postServer<GetAgentSessionStatusParameters, AgentSessionOperationResult> (\n            parameters |> Common.ensureCorrelationIdIsSet,\n            \"agent/session/status\"\n        )\n\n    /// Gets the active agent session for the current context.\n    static member public Active(parameters: GetActiveAgentSessionParameters) =\n        Common.postServer<GetActiveAgentSessionParameters, AgentSessionOperationResult> (\n            parameters |> Common.ensureCorrelationIdIsSet,\n            \"agent/session/active\"\n        )\n\n    /// Lists active agent sessions for the current context.\n    static member public ListActive(parameters: ListActiveAgentSessionsParameters) =\n        Common.postServer<ListActiveAgentSessionsParameters, AgentSessionListResult> (\n            parameters |> Common.ensureCorrelationIdIsSet,\n            \"agent/session/listActive\"\n        )\n\n    /// Alias for Start, named to match CLI `agent work start`.\n    static member public StartWork(parameters: StartAgentSessionParameters) = AgentSession.Start(parameters)\n\n    /// Alias for Stop, named to match CLI `agent work stop`.\n    static member public StopWork(parameters: StopAgentSessionParameters) = AgentSession.Stop(parameters)\n\n    /// Alias for Status, named to match CLI `agent work status`.\n    static member public GetWorkStatus(parameters: GetAgentSessionStatusParameters) = AgentSession.Status(parameters)\n"
  },
  {
    "path": "src/Grace.SDK/Diff.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared\nopen Grace.Types.Diff\nopen Grace.Shared.Parameters.Diff\nopen Grace.Types.Types\nopen System\nopen System.Threading.Tasks\n\ntype Diff() =\n    /// Gets a diff between two directory versions by DirectoryId's.\n    static member public GetDiff(parameters: GetDiffParameters) =\n        postServer<GetDiffParameters, DiffDto> (parameters |> ensureCorrelationIdIsSet, $\"diff/{nameof (Diff.GetDiff)}\")\n\n    /// Gets a diff between two directory versions by Sha256Hash.\n    static member public GetDiffBySha256Hash(parameters: GetDiffBySha256HashParameters) =\n        postServer<GetDiffBySha256HashParameters, DiffDto> (parameters |> ensureCorrelationIdIsSet, $\"diff/{nameof (Diff.GetDiffBySha256Hash)}\")\n\n    /// Populates the diff between two directory versions by DirectoryId's.\n    static member public Populate(parameters: PopulateParameters) =\n        postServer<PopulateParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"diff/{nameof (Diff.Populate)}\")\n"
  },
  {
    "path": "src/Grace.SDK/DirectoryVersion.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared\nopen Grace.Types\nopen Grace.Types.DirectoryVersion\nopen Grace.Shared.Utilities\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\ntype DirectoryVersion() =\n    /// Retrieves a DirectoryVersion instance.\n    static member public Get(parameters: GetParameters) =\n        postServer<GetParameters, DirectoryVersionDto> (parameters |> ensureCorrelationIdIsSet, $\"directory/{nameof (DirectoryVersion.Get)}\")\n\n    /// Retrieves a DirectoryVersion instance.\n    static member public GetByDirectoryIds(parameters: GetByDirectoryIdsParameters) =\n        postServer<GetByDirectoryIdsParameters, IEnumerable<DirectoryVersionDto>> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"directory/{nameof (DirectoryVersion.GetByDirectoryIds)}\"\n        )\n\n    /// Retrieves a DirectoryVersion instance.\n    static member public GetBySha256Hash(parameters: GetBySha256HashParameters) =\n        postServer<GetBySha256HashParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"directory/{nameof (DirectoryVersion.GetBySha256Hash)}\")\n\n    /// Retrieves the Uri to download the .zip file for a specific DirectoryVersion.\n    static member public GetZipFile(parameters: GetZipFileParameters) =\n        postServer<GetZipFileParameters, Types.UriWithSharedAccessSignature> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"directory/{nameof (DirectoryVersion.GetZipFile)}\"\n        )\n\n    /// Saves a list of DirectoryVersion instances to the server.\n    static member public Create(parameters: CreateParameters) =\n        postServer<CreateParameters, string> (parameters |> ensureCorrelationIdIsSet, $\"directory/{nameof (DirectoryVersion.Create)}\")\n\n    /// Saves a list of DirectoryVersion instances to the server.\n    static member public SaveDirectoryVersions(parameters: SaveDirectoryVersionsParameters) =\n        postServer<SaveDirectoryVersionsParameters, string> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"directory/{nameof (DirectoryVersion.SaveDirectoryVersions)}\"\n        )\n\n    /// Retrieves the recursive set of DirectoryVersions from a specific DirectoryVersion.\n    static member public GetDirectoryVersionsRecursive(parameters: GetParameters) =\n        postServer<GetParameters, IEnumerable<DirectoryVersionDto>> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"directory/{nameof (DirectoryVersion.GetDirectoryVersionsRecursive)}\"\n        )\n"
  },
  {
    "path": "src/Grace.SDK/Grace.SDK.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n\t\t<LangVersion>preview</LangVersion>\n\t\t<PublishReadyToRun>true</PublishReadyToRun>\n\t\t<GenerateDocumentationFile>true</GenerateDocumentationFile>\n\t\t<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <WarnOn>3390;$(WarnOn)</WarnOn>\n\t\t<WarningsAsErrors>FS0025</WarningsAsErrors>\n\t\t<NoWarn>1057,3391</NoWarn>\n\t\t<Platforms>AnyCPU</Platforms>\n\t\t<OtherFlags>--test:GraphBasedChecking</OtherFlags>\n\t\t<OtherFlags>--test:ParallelOptimization</OtherFlags>\n\t\t<OtherFlags>--test:ParallelIlxGen</OtherFlags>\n\t</PropertyGroup>\n\t<ItemGroup>\n        <None Include=\"instructions.md\" />\n        <Compile Include=\"Auth.SDK.fs\" />\n        <Compile Include=\"Common.SDK.fs\" />\n        <Compile Include=\"PersonalAccessToken.SDK.fs\" />\n        <Compile Include=\"Storage.SDK.fs\" />\n\t\t<Compile Include=\"Branch.SDK.fs\" />\n\t\t<Compile Include=\"Owner.SDK.fs\" />\n\t\t<Compile Include=\"Organization.SDK.fs\" />\n\t\t<Compile Include=\"Repository.SDK.fs\" />\n\t\t<Compile Include=\"DirectoryVersion.SDK.fs\" />\n\t\t<Compile Include=\"Diff.SDK.fs\" />\n                \n                <Compile Include=\"WorkItem.SDK.fs\" />\n                <Compile Include=\"Policy.SDK.fs\" />\n                <Compile Include=\"PromotionSet.SDK.fs\" />\n                <Compile Include=\"Review.SDK.fs\" />\n                <Compile Include=\"Queue.SDK.fs\" />\n                <Compile Include=\"ValidationSet.SDK.fs\" />\n                <Compile Include=\"ValidationResult.SDK.fs\" />\n                <Compile Include=\"Artifact.SDK.fs\" />\n                <Compile Include=\"Admin.SDK.fs\" />\n                <Compile Include=\"Access.SDK.fs\" />\n        </ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Azure.Storage.Blobs\" Version=\"12.26.0\" />\n\t\t<PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n\t\t<PackageReference Include=\"Polly\" Version=\"8.6.5\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n\t</ItemGroup>\n\n\t<ItemGroup>\n\t\t<PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n\t</ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.SDK/Organization.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Types.Organization\nopen Grace.Shared.Parameters.Organization\nopen System\n\ntype Organization() =\n\n    /// <summary>\n    /// Creates a new organization.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when creating the new organization.</param>\n    static member public Create(parameters: CreateOrganizationParameters) =\n        postServer<CreateOrganizationParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"organization/{nameof (Organization.Create)}\")\n\n    /// <summary>\n    /// Sets the organization's name.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the organization name.</param>\n    static member public Get(parameters: GetOrganizationParameters) =\n        postServer<GetOrganizationParameters, OrganizationDto> (parameters |> ensureCorrelationIdIsSet, $\"organization/{nameof (Organization.Get)}\")\n\n    /// <summary>\n    /// Sets the organization's name.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the organization name.</param>\n    static member public SetName(parameters: SetOrganizationNameParameters) =\n        postServer<SetOrganizationNameParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"organization/{nameof (Organization.SetName)}\")\n\n    /// <summary>\n    /// Sets the organization's type.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the organization's type.</param>\n    static member public SetType(parameters: SetOrganizationTypeParameters) =\n        postServer<SetOrganizationTypeParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"organization/{nameof (Organization.SetType)}\")\n\n    /// <summary>\n    /// Sets the organization's visibility in search results.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the search visibility.</param>\n    static member public SetSearchVisibility(parameters: SetOrganizationSearchVisibilityParameters) =\n        postServer<SetOrganizationSearchVisibilityParameters, String> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"organization/{nameof (Organization.SetSearchVisibility)}\"\n        )\n\n    /// <summary>\n    /// Sets the organization's description.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the organization's description.</param>\n    static member public SetDescription(parameters: SetOrganizationDescriptionParameters) =\n        postServer<SetOrganizationDescriptionParameters, String> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"organization/{nameof (Organization.SetDescription)}\"\n        )\n\n    /// <summary>\n    /// Deletes the organization.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when deleting the organization.</param>\n    static member public Delete(parameters: DeleteOrganizationParameters) =\n        postServer<DeleteOrganizationParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"organization/{nameof (Organization.Delete)}\")\n\n    /// <summary>\n    /// Undeletes the organization.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when deleting the owner.</param>\n    static member public Undelete(parameters: UndeleteOrganizationParameters) =\n        postServer<UndeleteOrganizationParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"organization/{nameof (Organization.Undelete)}\")\n"
  },
  {
    "path": "src/Grace.SDK/Owner.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Owner\nopen Grace.Types.Owner\nopen Grace.Types.Types\nopen System\nopen System.Threading.Tasks\n\ntype Owner() =\n\n    /// <summary>\n    /// Creates a new owner.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when creating the new owner.</param>\n    static member public Create(parameters: CreateOwnerParameters) =\n        postServer<CreateOwnerParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.Create)}\")\n\n    /// <summary>\n    /// Gets the owner's information.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when retrieving the owner information.</param>\n    static member public Get(parameters: GetOwnerParameters) =\n        postServer<GetOwnerParameters, OwnerDto> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.Get)}\")\n\n    /// <summary>\n    /// Sets the owner's name.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the owner name.</param>\n    static member public SetName(parameters: SetOwnerNameParameters) =\n        postServer<SetOwnerNameParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.SetName)}\")\n\n    /// <summary>\n    /// Sets the owner's type.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the owner type.</param>\n    static member public SetType(parameters: SetOwnerTypeParameters) =\n        postServer<SetOwnerTypeParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.SetType)}\")\n\n    /// <summary>\n    /// Sets the owner's visibility in search results.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the search visibility.</param>\n    static member public SetSearchVisibility(parameters: SetOwnerSearchVisibilityParameters) =\n        postServer<SetOwnerSearchVisibilityParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.SetSearchVisibility)}\")\n\n    /// <summary>\n    /// Sets the owner's description.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the owner's description.</param>\n    static member public SetDescription(parameters: SetOwnerDescriptionParameters) =\n        postServer<SetOwnerDescriptionParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.SetDescription)}\")\n\n    /// <summary>\n    /// Deletes the owner.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when deleting the owner.</param>\n    static member public Delete(parameters: DeleteOwnerParameters) =\n        postServer<DeleteOwnerParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.Delete)}\")\n\n    /// <summary>\n    /// Undeletes the owner.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when deleting the owner.</param>\n    static member public Undelete(parameters: UndeleteOwnerParameters) =\n        postServer<UndeleteOwnerParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"owner/{nameof (Owner.Undelete)}\")\n"
  },
  {
    "path": "src/Grace.SDK/PersonalAccessToken.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared.Parameters.Auth\nopen Grace.Shared.Services\nopen Grace.Shared.Utilities\nopen Grace.Types.PersonalAccessToken\nopen Grace.Types.Types\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\nopen System.Threading.Tasks\n\nmodule PersonalAccessToken =\n\n    let private postServerWithEnv<'T, 'U when 'T :> CommonParameters> (parameters: 'T, route: string) =\n        task {\n            try\n                let parameters = Common.ensureCorrelationIdIsSet parameters\n                use httpClient = getHttpClient parameters.CorrelationId\n                do! Auth.addAuthorizationHeader httpClient\n                let startTime = getCurrentInstant ()\n\n                let graceServerUri = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GraceServerUri)\n                let serverUri = Uri($\"{graceServerUri}/{route}\")\n\n                let! response = httpClient.PostAsync(serverUri, createJsonContent parameters)\n                let endTime = getCurrentInstant ()\n\n                if response.IsSuccessStatusCode then\n                    let! graceReturnValue = response.Content.ReadFromJsonAsync<GraceReturnValue<'U>>(Constants.JsonSerializerOptions)\n\n                    return\n                        Ok graceReturnValue\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                else if response.StatusCode = HttpStatusCode.NotFound then\n                    return Error(GraceError.Create $\"Server endpoint {route} not found.\" parameters.CorrelationId)\n                elif response.StatusCode = HttpStatusCode.BadRequest then\n                    let! errorMessage = response.Content.ReadAsStringAsync()\n\n                    return\n                        Error(GraceError.Create $\"{errorMessage}\" parameters.CorrelationId)\n                        |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                        |> enhance \"StatusCode\" $\"{response.StatusCode}\"\n                else\n                    let! responseAsString = response.Content.ReadAsStringAsync()\n\n                    try\n                        let graceError = deserialize<GraceError> responseAsString\n\n                        return\n                            Error graceError\n                            |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                            |> enhance \"StatusCode\" $\"{response.StatusCode}\"\n                    with\n                    | _ ->\n                        return\n                            Error(GraceError.Create $\"{responseAsString}\" parameters.CorrelationId)\n                            |> enhance \"ServerResponseTime\" $\"{(endTime - startTime).TotalMilliseconds:F3} ms\"\n                            |> enhance \"StatusCode\" $\"{response.StatusCode}\"\n            with\n            | ex ->\n                let exceptionResponse = Utilities.ExceptionResponse.Create ex\n                return Error(GraceError.Create ($\"{exceptionResponse}\") parameters.CorrelationId)\n        }\n\n    let Create (parameters: CreatePersonalAccessTokenParameters) =\n        postServerWithEnv<CreatePersonalAccessTokenParameters, PersonalAccessTokenCreated> (parameters, \"auth/token/create\")\n\n    let List (parameters: ListPersonalAccessTokensParameters) =\n        postServerWithEnv<ListPersonalAccessTokensParameters, PersonalAccessTokenSummary list> (parameters, \"auth/token/list\")\n\n    let Revoke (parameters: RevokePersonalAccessTokenParameters) =\n        postServerWithEnv<RevokePersonalAccessTokenParameters, PersonalAccessTokenSummary> (parameters, \"auth/token/revoke\")\n"
  },
  {
    "path": "src/Grace.SDK/Policy.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Policy\nopen Grace.Types.Policy\nopen System.Threading.Tasks\n\n/// The Policy module provides a set of functions for interacting with policy snapshots in the Grace API.\ntype Policy() =\n    /// Gets the current policy snapshot for a target branch.\n    static member public GetCurrent(parameters: GetPolicyParameters) =\n        postServer<GetPolicyParameters, PolicySnapshot option> (parameters |> ensureCorrelationIdIsSet, \"policy/current\")\n\n    /// Acknowledges a policy snapshot.\n    static member public Acknowledge(parameters: AcknowledgePolicyParameters) =\n        postServer<AcknowledgePolicyParameters, string> (parameters |> ensureCorrelationIdIsSet, \"policy/acknowledge\")\n"
  },
  {
    "path": "src/Grace.SDK/PromotionSet.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.PromotionSet\nopen Grace.Types.PromotionSet\nopen System.Collections.Generic\n\n/// The PromotionSet module provides a set of functions for interacting with promotion sets in the Grace API.\ntype PromotionSet() =\n    /// Creates a promotion set.\n    static member public Create(parameters: CreatePromotionSetParameters) =\n        postServer<CreatePromotionSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/create\")\n\n    /// Gets a promotion set by ID.\n    static member public Get(parameters: GetPromotionSetParameters) =\n        postServer<GetPromotionSetParameters, PromotionSetDto> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/get\")\n\n    /// Gets promotion set events by ID.\n    static member public GetEvents(parameters: GetPromotionSetEventsParameters) =\n        postServer<GetPromotionSetEventsParameters, IReadOnlyList<PromotionSetEvent>> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/get-events\")\n\n    /// Updates the input promotion pointers for a promotion set.\n    static member public UpdateInputPromotions(parameters: UpdatePromotionSetInputPromotionsParameters) =\n        postServer<UpdatePromotionSetInputPromotionsParameters, string> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/update-input-promotions\")\n\n    /// Requests server-side step recomputation for a promotion set.\n    static member public Recompute(parameters: RecomputePromotionSetParameters) =\n        postServer<RecomputePromotionSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/recompute\")\n\n    /// Applies a promotion set.\n    static member public Apply(parameters: ApplyPromotionSetParameters) =\n        postServer<ApplyPromotionSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/apply\")\n\n    /// Resolves blocked promotion set conflicts.\n    static member public ResolveConflicts(parameters: ResolvePromotionSetConflictsParameters) =\n        postServer<ResolvePromotionSetConflictsParameters, string> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"promotion-set/{parameters.PromotionSetId}/resolve-conflicts\"\n        )\n\n    /// Logically deletes a promotion set.\n    static member public Delete(parameters: DeletePromotionSetParameters) =\n        postServer<DeletePromotionSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"promotion-set/delete\")\n"
  },
  {
    "path": "src/Grace.SDK/Queue.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Queue\nopen Grace.Types.Queue\nopen System.Threading.Tasks\n\n/// The Queue module provides a set of functions for interacting with promotion queues in the Grace API.\ntype Queue() =\n    /// Gets the status of a promotion queue for a target branch.\n    static member public Status(parameters: QueueStatusParameters) =\n        postServer<QueueStatusParameters, PromotionQueue> (parameters |> ensureCorrelationIdIsSet, \"queue/status\")\n\n    /// Enqueues a promotion set in a promotion queue.\n    static member public Enqueue(parameters: EnqueueParameters) =\n        postServer<EnqueueParameters, string> (parameters |> ensureCorrelationIdIsSet, \"queue/enqueue\")\n\n    /// Pauses a promotion queue.\n    static member public Pause(parameters: QueueActionParameters) =\n        postServer<QueueActionParameters, string> (parameters |> ensureCorrelationIdIsSet, \"queue/pause\")\n\n    /// Resumes a promotion queue.\n    static member public Resume(parameters: QueueActionParameters) =\n        postServer<QueueActionParameters, string> (parameters |> ensureCorrelationIdIsSet, \"queue/resume\")\n\n    /// Dequeues a promotion set from a promotion queue.\n    static member public Dequeue(parameters: PromotionSetActionParameters) =\n        postServer<PromotionSetActionParameters, string> (parameters |> ensureCorrelationIdIsSet, \"queue/dequeue\")\n"
  },
  {
    "path": "src/Grace.SDK/Repository.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Repository\nopen Grace.Shared.Utilities\nopen Grace.Types.Branch\nopen Grace.Types.Reference\nopen Grace.Types.Repository\nopen Grace.Types.Types\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\ntype Repository() =\n\n    /// <summary>\n    /// Creates a new repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when creating the new repository.</param>\n    static member public Create(parameters: CreateRepositoryParameters) =\n        logToConsole $\"Creating repository: RepositoryId: {parameters.RepositoryId}; RepositoryName: {parameters.RepositoryName}.\"\n        postServer<CreateRepositoryParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.Create)}\")\n\n    /// <summary>\n    /// Creates a new repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when creating the new repository.</param>\n    static member public Init(parameters: InitParameters) =\n        postServer<InitParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.Init)}\")\n\n    /// Checks to see if a repository is empty and ready for initialization.\n    static member public IsEmpty(parameters: IsEmptyParameters) =\n        postServer<IsEmptyParameters, bool> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.IsEmpty)}\")\n\n    /// <summary>\n    /// Sets the name of the repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the status of the repository.</param>\n    static member public SetName(parameters: SetRepositoryNameParameters) =\n        postServer<SetRepositoryNameParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetName)}\")\n\n    /// <summary>\n    /// Sets the status of the repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the status of the repository.</param>\n    static member public SetStatus(parameters: SetRepositoryStatusParameters) =\n        postServer<SetRepositoryStatusParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetStatus)}\")\n\n    /// Sets the anonymous access status of the repository.\n    static member public SetAnonymousAccess(parameters: SetAnonymousAccessParameters) =\n        postServer<SetAnonymousAccessParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetAnonymousAccess)}\")\n\n    /// Sets the large file support status of the repository.\n    static member public SetAllowsLargeFiles(parameters: SetAllowsLargeFilesParameters) =\n        postServer<SetAllowsLargeFilesParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetAllowsLargeFiles)}\")\n\n    /// <summary>\n    /// Sets whether the repository is public or private.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the repository visibility.</param>\n    static member public SetVisibility(parameters: SetRepositoryVisibilityParameters) =\n        postServer<SetRepositoryVisibilityParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetVisibility)}\")\n\n    /// <summary>\n    /// Sets the repository's description.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the repository description.</param>\n    static member public SetDescription(parameters: SetRepositoryDescriptionParameters) =\n        postServer<SetRepositoryDescriptionParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetDescription)}\")\n\n    /// <summary>\n    /// Sets the repository default for whether saves should be kept.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the repository default for whether saves should be kept.</param>\n    static member public SetRecordSaves(parameters: RecordSavesParameters) =\n        postServer<RecordSavesParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetRecordSaves)}\")\n\n    /// Sets the number of days to keep logical deletes in this repository.\n    static member public SetLogicalDeleteDays(parameters: SetLogicalDeleteDaysParameters) =\n        postServer<SetLogicalDeleteDaysParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetLogicalDeleteDays)}\")\n\n    /// <summary>\n    /// Sets the number of days to keep saves in this repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the default save days.</param>\n    static member public SetSaveDays(parameters: SetSaveDaysParameters) =\n        postServer<SetSaveDaysParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetSaveDays)}\")\n\n    /// <summary>\n    /// Sets the number of days to keep checkpoints in this repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the default checkpoint retention time.</param>\n    static member public SetCheckpointDays(parameters: SetCheckpointDaysParameters) =\n        postServer<SetCheckpointDaysParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetCheckpointDays)}\")\n\n    /// Sets the number of days to keep diff results cached in the database.\n    static member public SetDiffCacheDays(parameters: SetDiffCacheDaysParameters) =\n        postServer<SetDiffCacheDaysParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.SetDiffCacheDays)}\")\n\n    /// Sets the number of days to keep recursive directory version contents in the database.\n    static member public SetDirectoryVersionCacheDays(parameters: SetDirectoryVersionCacheDaysParameters) =\n        postServer<SetDirectoryVersionCacheDaysParameters, String> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"repository/{nameof (Repository.SetDirectoryVersionCacheDays)}\"\n        )\n\n    /// <summary>\n    /// Sets the default version of the Server API that clients should use when accessing this repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when setting the default checkpoint retention time.</param>\n    static member public SetDefaultServerApiVersion(parameters: SetDefaultServerApiVersionParameters) =\n        postServer<SetDefaultServerApiVersionParameters, String> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"repository/{nameof (Repository.SetDefaultServerApiVersion)}\"\n        )\n\n    /// <summary>\n    /// Deletes the repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when deleting the repository.</param>\n    static member public Delete(parameters: DeleteRepositoryParameters) =\n        postServer<DeleteRepositoryParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.Delete)}\")\n\n    /// <summary>\n    /// Undeletes the repository.\n    /// </summary>\n    /// <param name=\"parameters\">Values to use when deleting the owner.</param>\n    static member public Undelete(parameters: UndeleteRepositoryParameters) =\n        postServer<UndeleteRepositoryParameters, String> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.Undelete)}\")\n\n    /// Gets the metadata for the repository.\n    static member public Get(parameters: RepositoryParameters) =\n        postServer<RepositoryParameters, RepositoryDto> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.Get)}\")\n\n    /// Gets a list of branches in the repository.\n    static member public GetBranches(parameters: GetBranchesParameters) =\n        postServer<GetBranchesParameters, IEnumerable<BranchDto>> (parameters |> ensureCorrelationIdIsSet, $\"repository/{nameof (Repository.GetBranches)}\")\n\n    /// Gets a list of branches in the repository.\n    static member public GetBranchesByBranchId(parameters: GetBranchesByBranchIdParameters) =\n        postServer<GetBranchesByBranchIdParameters, IEnumerable<BranchDto>> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"repository/{nameof (Repository.GetBranchesByBranchId)}\"\n        )\n\n    /// Gets a list of references in a repository, which may be in different branches.\n    static member public GetReferencesByReferenceId(parameters: GetReferencesByReferenceIdParameters) =\n        postServer<GetReferencesByReferenceIdParameters, IEnumerable<ReferenceDto>> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"repository/{nameof (Repository.GetReferencesByReferenceId)}\"\n        )\n\n    /// Sets the conflict resolution policy for the repository.\n    static member public SetConflictResolutionPolicy(parameters: SetConflictResolutionPolicyParameters) =\n        postServer<SetConflictResolutionPolicyParameters, String> (\n            parameters |> ensureCorrelationIdIsSet,\n            $\"repository/{nameof (Repository.SetConflictResolutionPolicy)}\"\n        )\n"
  },
  {
    "path": "src/Grace.SDK/Review.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Review\nopen Grace.Types.Review\nopen System\nopen System.Threading.Tasks\n\n[<RequireQualifiedAccess>]\nmodule ReviewReportSchema =\n    [<Literal>]\n    let Version = \"1.0\"\n\n[<RequireQualifiedAccess>]\nmodule ReviewReportSections =\n    [<Literal>]\n    let CandidateAndPromotionSet = \"candidate-and-promotion-set\"\n\n    [<Literal>]\n    let QueueAndRequiredActions = \"queue-and-required-actions\"\n\n    [<Literal>]\n    let ValidationAndGateOutcomes = \"validation-and-gate-outcomes\"\n\n    [<Literal>]\n    let ReviewNotesAndCheckpoint = \"review-notes-and-checkpoint\"\n\n    [<Literal>]\n    let WorkItemLinksAndArtifacts = \"work-item-links-and-artifacts\"\n\n    [<Literal>]\n    let BlockingReasonsAndNextActions = \"blocking-reasons-and-next-actions\"\n\n    let Ordered =\n        [\n            CandidateAndPromotionSet\n            QueueAndRequiredActions\n            ValidationAndGateOutcomes\n            ReviewNotesAndCheckpoint\n            WorkItemLinksAndArtifacts\n            BlockingReasonsAndNextActions\n        ]\n\ntype ReviewReportEntry() =\n    member val public Key = String.Empty with get, set\n    member val public Values: string list = [] with get, set\n\ntype ReviewReportSection() =\n    member val public Section = String.Empty with get, set\n    member val public Title = String.Empty with get, set\n    member val public SourceState = ProjectionSourceStates.NotAvailable with get, set\n    member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n    member val public Entries: ReviewReportEntry list = [] with get, set\n    member val public Diagnostics: string list = [] with get, set\n\ntype ReviewReportResult() =\n    member val public ReviewReportSchemaVersion = ReviewReportSchema.Version with get, set\n    member val public SectionOrder: string list = ReviewReportSections.Ordered with get, set\n    member val public Sections: ReviewReportSection list = [] with get, set\n\n/// The Review module provides a set of functions for interacting with reviews in the Grace API.\ntype Review() =\n    /// Gets review notes for a promotion set.\n    static member public GetNotes(parameters: GetReviewNotesParameters) =\n        postServer<GetReviewNotesParameters, ReviewNotes option> (parameters |> ensureCorrelationIdIsSet, \"review/notes\")\n\n    /// Records a review checkpoint.\n    static member public Checkpoint(parameters: ReviewCheckpointParameters) =\n        postServer<ReviewCheckpointParameters, string> (parameters |> ensureCorrelationIdIsSet, \"review/checkpoint\")\n\n    /// Resolves a review finding.\n    static member public ResolveFinding(parameters: ResolveFindingParameters) =\n        postServer<ResolveFindingParameters, string> (parameters |> ensureCorrelationIdIsSet, \"review/resolve\")\n\n    /// Requests deeper analysis for a promotion set.\n    static member public Deepen(parameters: DeepenReviewParameters) =\n        postServer<DeepenReviewParameters, string> (parameters |> ensureCorrelationIdIsSet, \"review/deepen\")\n\n    /// Resolves candidate identity to a promotion-set-backed projection identity.\n    static member public ResolveCandidateIdentity(parameters: ResolveCandidateIdentityParameters) =\n        postServer<ResolveCandidateIdentityParameters, CandidateIdentityProjectionResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/resolve\")\n\n    /// Gets candidate projection details.\n    static member public GetCandidate(parameters: CandidateProjectionParameters) =\n        postServer<CandidateProjectionParameters, CandidateProjectionSnapshotResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/get\")\n\n    /// Gets candidate required actions.\n    static member public GetCandidateRequiredActions(parameters: CandidateProjectionParameters) =\n        postServer<CandidateProjectionParameters, CandidateRequiredActionsResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/required-actions\")\n\n    /// Gets candidate attestations.\n    static member public GetCandidateAttestations(parameters: CandidateProjectionParameters) =\n        postServer<CandidateProjectionParameters, CandidateAttestationsResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/attestations\")\n\n    /// Retries candidate processing through PromotionSet recompute and queue operations.\n    static member public RetryCandidate(parameters: CandidateProjectionParameters) =\n        postServer<CandidateProjectionParameters, CandidateActionResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/retry\")\n\n    /// Cancels candidate queue processing when queued.\n    static member public CancelCandidate(parameters: CandidateProjectionParameters) =\n        postServer<CandidateProjectionParameters, CandidateActionResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/cancel\")\n\n    /// Requests candidate gate rerun semantics.\n    static member public RerunCandidateGate(parameters: CandidateGateRerunParameters) =\n        postServer<CandidateGateRerunParameters, CandidateActionResult> (parameters |> ensureCorrelationIdIsSet, \"review/candidate/gate-rerun\")\n\n    /// Gets a unified review report for candidate-first reviewer workflows.\n    static member public GetReviewReport(parameters: CandidateProjectionParameters) =\n        postServer<CandidateProjectionParameters, ReviewReportResult> (parameters |> ensureCorrelationIdIsSet, \"review/report/get\")\n"
  },
  {
    "path": "src/Grace.SDK/Storage.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Azure.Core.Pipeline\nopen Azure.Storage\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Models\nopen Azure.Storage.Blobs.Specialized\nopen FSharpPlus\nopen Grace.SDK.Common\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Services\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen NodaTime.Text\nopen System\nopen System.Buffers\nopen System.Collections.Generic\nopen System.Globalization\nopen System.IO\nopen System.IO.Compression\nopen System.IO.Enumeration\nopen System.Linq\nopen System.Net\nopen System.Net.Http.Json\nopen System.Threading.Tasks\nopen System.Text\nopen Grace.Shared.Parameters.Storage\nopen Azure\n\nmodule Storage =\n\n    /// Gets a file from object storage and saves it to the local object directory.\n    let GetFileFromObjectStorage (getDownloadUriParameters: GetDownloadUriParameters) correlationId =\n        task {\n            let fileVersion = getDownloadUriParameters.FileVersion\n\n            try\n                match Current().ObjectStorageProvider with\n                | AzureBlobStorage ->\n                    // Get the URI to use when downloading the file. This includes a SAS token.\n                    let httpClient = getHttpClient correlationId\n                    do! Auth.addAuthorizationHeader httpClient\n                    let serviceUrl = $\"{Current().ServerUri}/storage/getDownloadUri\"\n                    let jsonContent = createJsonContent getDownloadUriParameters\n                    let! response = httpClient.PostAsync(serviceUrl, jsonContent)\n                    let! blobUriWithSasToken = response.Content.ReadAsStringAsync()\n                    //logToConsole $\"response.StatusCode: {response.StatusCode}; blobUriWithSasToken: {blobUriWithSasToken}\"\n\n                    let relativeDirectory =\n                        if fileVersion.RelativeDirectory = Constants.RootDirectoryPath then\n                            String.Empty\n                        else\n                            getNativeFilePath fileVersion.RelativeDirectory\n\n                    let tempFilePath = Path.Combine(Path.GetTempPath(), relativeDirectory, fileVersion.GetObjectFileName)\n\n                    let objectFilePath = Path.Combine(Current().ObjectDirectory, fileVersion.RelativePath, fileVersion.GetObjectFileName)\n\n                    let tempFileInfo = FileInfo(tempFilePath)\n                    let objectFileInfo = FileInfo(objectFilePath)\n\n                    Directory.CreateDirectory(tempFileInfo.Directory.FullName)\n                    |> ignore\n\n                    Directory.CreateDirectory(objectFileInfo.Directory.FullName)\n                    |> ignore\n                    //logToConsole $\"tempFilePath: {tempFilePath}; objectFilePath: {objectFilePath}\"\n\n                    // Download the file from object storage.\n                    let blobClient = BlobClient(Uri(blobUriWithSasToken))\n                    let! azureResponse = blobClient.DownloadToAsync(tempFilePath)\n\n                    if not <| azureResponse.IsError then\n                        //File.Move(tempFilePath, objectFilePath, overwrite = true)\n                        if fileVersion.IsBinary then\n                            File.Move(tempFilePath, objectFilePath, overwrite = true)\n                        else\n                            use tempFileStream = tempFileInfo.OpenRead()\n                            use gzStream = new GZipStream(tempFileStream, CompressionMode.Decompress, leaveOpen = false)\n                            use fileWriter = objectFileInfo.OpenWrite()\n\n                            do! gzStream.CopyToAsync(fileWriter)\n                            //logToConsole $\"In GetFileFromObjectStorage: After CopyToAsync(). {fileVersion.RelativePath}\"\n\n                            do! fileWriter.FlushAsync()\n                        //logToConsole $\"In GetFileFromObjectStorage: After FlushAsync(). {fileVersion.RelativePath}\"\n\n                        //logToConsole $\"After tempFileInfo.Delete(). {fileVersion.RelativePath}\"\n\n                        tempFileInfo.Delete()\n                        return Ok(GraceReturnValue.Create \"Retrieved all files from object storage.\" correlationId)\n                    else\n                        tempFileInfo.Delete()\n\n                        let error = GraceError.Create (getErrorMessage StorageError.FailedCommunicatingWithObjectStorage) correlationId\n\n                        error.Properties.Add(\"StatusCode\", $\"HTTP {azureResponse.Status}\")\n                        error.Properties.Add(\"ReasonPhrase\", $\"Reason: {azureResponse.ReasonPhrase}\")\n                        return Error error\n                | AWSS3 -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                | GoogleCloudStorage -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                | ObjectStorageProvider.Unknown -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n            with\n            | ex ->\n                logToConsole $\"Exception downloading {fileVersion.GetObjectFileName}: {ex.Message}\"\n                return Error(GraceError.Create (getErrorMessage StorageError.ObjectStorageException) correlationId)\n        }\n\n    /// Gets upload metadata (including upload URLs with SAS tokens) for a list of files to be uploaded to object storage.\n    let GetUploadMetadataForFiles (parameters: GetUploadMetadataForFilesParameters) =\n        task {\n            let correlationId = parameters.CorrelationId\n\n            try\n                if parameters.FileVersions.Length > 0 then\n                    match Current().ObjectStorageProvider with\n                    | AzureBlobStorage ->\n                        let httpClient = getHttpClient correlationId\n                        do! Auth.addAuthorizationHeader httpClient\n                        let serviceUrl = $\"{Current().ServerUri}/storage/getUploadMetadataForFiles\"\n                        let jsonContent = createJsonContent parameters\n                        let! response = httpClient.PostAsync(serviceUrl, jsonContent)\n\n                        if response.IsSuccessStatusCode then\n                            let! uploadMetadata = response.Content.ReadFromJsonAsync<GraceReturnValue<List<UploadMetadata>>>(Constants.JsonSerializerOptions)\n\n                            return Ok uploadMetadata\n                        else\n                            let! errorMessage = response.Content.ReadAsStringAsync()\n\n                            let graceError = (GraceError.Create $\"{getErrorMessage StorageError.FailedToGetUploadUrls}; {errorMessage}\" correlationId)\n\n                            let fileVersionList = StringBuilder()\n\n                            for fileVersion in parameters.FileVersions do\n                                fileVersionList.Append($\"{fileVersion.RelativePath}; \")\n                                |> ignore\n\n                            return\n                                Error graceError\n                                |> enhance \"fileVersions\" $\"{fileVersionList}\"\n                    | AWSS3 -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                    | GoogleCloudStorage -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                    | ObjectStorageProvider.Unknown -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                else\n                    return Error(GraceError.Create (getErrorMessage StorageError.FilesMustNotBeEmpty) correlationId)\n            with\n            | ex ->\n                let exceptionResponse = ExceptionResponse.Create ex\n                return Error(GraceError.Create (exceptionResponse.ToString()) correlationId)\n        }\n\n    let storageTransferOptions = StorageTransferOptions(MaximumConcurrency = Constants.ParallelOptions.MaxDegreeOfParallelism)\n\n    /// Saves a file to object storage with the specified metadata.\n    let SaveFileToObjectStorageWithMetadata\n        (repositoryId: RepositoryId)\n        (fileVersion: FileVersion)\n        (blobUriWithSasToken: Uri)\n        (metadata: Dictionary<string, string>)\n        correlationId\n        =\n        task {\n            try\n                //logToConsole $\"In SDK.Storage.SaveFileToObjectStorageWithMetadata: fileVersion.RelativePath: {fileVersion.RelativePath}.\"\n                let fileInfo = FileInfo(Path.Combine(Current().RootDirectory, fileVersion.RelativePath))\n\n                metadata[nameof CorrelationId] <- correlationId\n                metadata[nameof OwnerId] <- $\"{Current().OwnerId}\"\n                metadata[nameof OrganizationId] <- $\"{Current().OrganizationId}\"\n                metadata[nameof RepositoryName] <- $\"{Current().RepositoryName}\"\n                metadata[nameof RepositoryId] <- $\"{Current().RepositoryId}\"\n                metadata[nameof Sha256Hash] <- fileVersion.Sha256Hash\n                metadata[\"UncompressedSize\"] <- $\"{fileInfo.Length}\"\n\n                match Current().ObjectStorageProvider with\n                | ObjectStorageProvider.Unknown -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                | ObjectStorageProvider.AzureBlobStorage ->\n                    try\n                        // Creating an HttpClientTransport so we can use our custom HttpClientFactory here.\n                        use transport = new HttpClientTransport(getHttpClient correlationId)\n\n                        let blobClientOptions = BlobClientOptions(Transport = transport)\n                        // I might regret this setting. Time will tell.\n                        blobClientOptions.Retry.NetworkTimeout <- TimeSpan.FromMinutes(60.0)\n\n                        let blockBlobClient = BlockBlobClient(blobUriWithSasToken, blobClientOptions)\n\n                        let blobUploadOptions = BlobUploadOptions(Metadata = metadata, Tags = metadata, TransferOptions = storageTransferOptions)\n                        // Setting IfNoneMatch = \"*\" tells Azure Storage not to upload the file if it already exists.\n                        //blobUploadOptions.Conditions <- new BlobRequestConditions(IfNoneMatch = ETag.All)\n\n                        // Add well-known headers to the blob.\n                        //   Content-Type: The MIME type of the file.\n                        //   Cache-Control: The maximum amount of time the file should be cached by a browser or proxy server.\n                        //   Content-Disposition: An attachment with the creation date of the file; setting this header tells a browser to use the SaveAs dialog, and to use correct name and original timestamp.\n                        blobUploadOptions.HttpHeaders <-\n                            BlobHttpHeaders(\n                                ContentType = getContentType fileInfo (fileVersion.IsBinary),\n                                CacheControl = Constants.BlobCacheControl,\n                                ContentDisposition =\n                                    $\"\"\"attachment; creation-date=\"{fileVersion.CreatedAt.ToString(InstantPattern.General.PatternText, CultureInfo.InvariantCulture)}\" \"\"\"\n                            )\n\n                        let objectFilePath =\n                            $\"{Current().ObjectDirectory}{Path.DirectorySeparatorChar}{fileVersion.RelativePath}{Path.DirectorySeparatorChar}{fileVersion.GetObjectFileName}\"\n\n                        let normalizedObjectFilePath = Path.GetFullPath(objectFilePath)\n\n                        use fileStream = File.Open(normalizedObjectFilePath, fileStreamOptionsRead)\n\n                        // We're setting Conditions in blobUploadOptions, so it won't overwrite existing files.\n                        //   Unfortunately, it will still try to upload the file, which is a waste of time and bandwidth.\n                        //   For larger files, I'm going to check if the file is already uploaded before trying to upload it.\n                        //   This is a performance and cost optimization; each call to ExistsAsync() costs money.\n                        //   By doing it this way, we trade some client upload bandwidth for saving time waiting for\n                        //   ExistsAsync() to return false, plus we're saving the cost of the ExistsAsync() call.\n                        let! status =\n                            task {\n                                // If the file is a binary file, stream it to Blob Storage without compressing it.\n                                if fileVersion.IsBinary then\n                                    if fileVersion.Size > 1024L * 1024L then\n                                        // If the file is larger than 1 MB, check if it is already uploaded to Blob Storage.\n                                        let! fileExists = blockBlobClient.ExistsAsync()\n\n                                        if fileExists.Value = true then\n                                            return int HttpStatusCode.Conflict\n                                        else\n                                            let! response = blockBlobClient.UploadAsync(fileStream, blobUploadOptions)\n                                            return response.GetRawResponse().Status\n                                    else\n                                        let! response = blockBlobClient.UploadAsync(fileStream, blobUploadOptions)\n                                        return response.GetRawResponse().Status\n                                else\n                                    // If the file is not a binary file, gzip it, and stream the compressed file to Blob Storage.\n                                    blobUploadOptions.HttpHeaders.ContentEncoding <- \"gzip\"\n                                    use memoryStream = new MemoryStream(64 * 1024) // Setting initial capacity larger than most files will need.\n                                    use gzipStream = new GZipStream(stream = memoryStream, compressionLevel = CompressionLevel.SmallestSize, leaveOpen = false)\n                                    do! fileStream.CopyToAsync(gzipStream, bufferSize = (64 * 1024))\n                                    do! gzipStream.FlushAsync()\n                                    memoryStream.Position <- 0\n\n                                    if memoryStream.Length > 1024L * 1024L then\n                                        // If the file is larger than 1 MB, check if it is already uploaded to Blob Storage.\n                                        let! fileExists = blockBlobClient.ExistsAsync()\n\n                                        if fileExists.Value = true then\n                                            return int HttpStatusCode.Conflict\n                                        else\n                                            let! response = blockBlobClient.UploadAsync(memoryStream, blobUploadOptions)\n                                            return response.GetRawResponse().Status\n                                    else\n                                        let! response = blockBlobClient.UploadAsync(memoryStream, blobUploadOptions)\n                                        return response.GetRawResponse().Status\n                            }\n\n                        //logToConsole $\"In SaveFileToObjectStorageWithMetadata: {fileVersion.RelativePath}; upload status: {status}.\"\n\n                        if status = int HttpStatusCode.Created then\n                            let returnValue = GraceReturnValue.Create \"File successfully saved to object storage.\" correlationId\n\n                            returnValue.Properties.Add(nameof Sha256Hash, fileVersion.Sha256Hash)\n                            returnValue.Properties.Add(nameof RelativePath, fileVersion.RelativePath)\n                            returnValue.Properties.Add(nameof RepositoryId, repositoryId)\n                            return Ok returnValue\n                        elif status = int HttpStatusCode.Conflict then\n                            // If the file already exists in Blob Storage, we don't need to do anything.\n                            let returnValue = GraceReturnValue.Create \"File already exists in object storage.\" correlationId\n\n                            returnValue.Properties.Add(nameof Sha256Hash, fileVersion.Sha256Hash)\n                            returnValue.Properties.Add(nameof RelativePath, fileVersion.RelativePath)\n                            returnValue.Properties.Add(nameof RepositoryId, repositoryId)\n                            return Ok returnValue\n                        else\n                            let error = (GraceError.Create $\"Failed to upload file {normalizedObjectFilePath} to object storage.\" correlationId)\n                            return Error error\n                    with\n                    | ex ->\n                        if ex.Message.Contains(\"The specified blob already exists.\") then\n                            // If the file already exists in Blob Storage, we don't need to do anything.\n                            let returnValue = GraceReturnValue.Create \"File already exists in object storage.\" correlationId\n                            returnValue.Properties.Add(nameof Sha256Hash, fileVersion.Sha256Hash)\n                            returnValue.Properties.Add(nameof RelativePath, fileVersion.RelativePath)\n                            returnValue.Properties.Add(nameof RepositoryId, repositoryId)\n                            return Ok returnValue\n                        else\n                            let exceptionResponse = ExceptionResponse.Create ex\n                            return Error(GraceError.Create (exceptionResponse.ToString()) correlationId)\n                | ObjectStorageProvider.AWSS3 -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n                | ObjectStorageProvider.GoogleCloudStorage -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) correlationId)\n            with\n            | ex ->\n                let exceptionResponse = ExceptionResponse.Create ex\n                return Error(GraceError.Create (exceptionResponse.ToString()) correlationId)\n        }\n\n    /// Saves a file to object storage.\n    let SaveFileToObjectStorage (repositoryId: RepositoryId) (fileVersion: FileVersion) (blobUriWithSasToken: Uri) correlationId =\n        SaveFileToObjectStorageWithMetadata repositoryId fileVersion blobUriWithSasToken (Dictionary<string, string>()) correlationId\n\n    /// Gets an upload URI with a SAS token for uploading a file to object storage.\n    let GetUploadUri (parameters: GetUploadUriParameters) =\n        task {\n            try\n                match Current().ObjectStorageProvider with\n                | ObjectStorageProvider.Unknown -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n                | ObjectStorageProvider.AzureBlobStorage ->\n                    let httpClient = getHttpClient parameters.CorrelationId\n                    do! Auth.addAuthorizationHeader httpClient\n                    let serviceUrl = $\"{Current().ServerUri}/storage/getUploadUri\"\n                    let jsonContent = createJsonContent parameters\n                    let! response = httpClient.PostAsync(serviceUrl, jsonContent)\n                    let! blobUriWithSasToken = response.Content.ReadAsStringAsync()\n                    //logToConsole $\"blobUriWithSasToken: {blobUriWithSasToken}\"\n                    return Ok(GraceReturnValue.Create blobUriWithSasToken parameters.CorrelationId)\n                | ObjectStorageProvider.AWSS3 -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n                | ObjectStorageProvider.GoogleCloudStorage ->\n                    return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n            with\n            | ex ->\n                let exceptionResponse = ExceptionResponse.Create ex\n                logToConsole $\"exception: {exceptionResponse.ToString()}\"\n                return Error(GraceError.Create (exceptionResponse.ToString()) parameters.CorrelationId)\n        }\n\n    /// Gets a download URI with a SAS token for downloading a file from object storage.\n    let GetDownloadUri (parameters: GetDownloadUriParameters) =\n        task {\n            try\n                match Current().ObjectStorageProvider with\n                | ObjectStorageProvider.Unknown -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n                | ObjectStorageProvider.AzureBlobStorage ->\n                    let httpClient = getHttpClient parameters.CorrelationId\n                    do! Auth.addAuthorizationHeader httpClient\n                    let serviceUrl = $\"{Current().ServerUri}/storage/getDownloadUri\"\n                    let jsonContent = createJsonContent parameters\n                    let! response = httpClient.PostAsync(serviceUrl, jsonContent)\n                    let! blobUriWithSasToken = response.Content.ReadAsStringAsync()\n                    //logToConsole $\"blobUriWithSasToken: {blobUriWithSasToken}\"\n                    return Ok(GraceReturnValue.Create blobUriWithSasToken parameters.CorrelationId)\n                | ObjectStorageProvider.AWSS3 -> return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n                | ObjectStorageProvider.GoogleCloudStorage ->\n                    return Error(GraceError.Create (getErrorMessage StorageError.NotImplemented) parameters.CorrelationId)\n            with\n            | ex ->\n                let exceptionResponse = ExceptionResponse.Create ex\n                logToConsole $\"exception: {exceptionResponse.ToString()}\"\n                return Error(GraceError.Create (exceptionResponse.ToString()) parameters.CorrelationId)\n        }\n"
  },
  {
    "path": "src/Grace.SDK/ValidationResult.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Validation\nopen Grace.Types.Validation\n\n/// Client API for validation result endpoints.\ntype ValidationResult() =\n\n    /// Records a validation result.\n    static member public Record(parameters: RecordValidationResultParameters) =\n        postServer<RecordValidationResultParameters, ValidationResultDto> (parameters |> ensureCorrelationIdIsSet, \"validation-result/record\")\n"
  },
  {
    "path": "src/Grace.SDK/ValidationSet.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.Validation\nopen Grace.Types.Validation\n\n/// Client API for validation set endpoints.\ntype ValidationSet() =\n\n    /// Creates a validation set.\n    static member public Create(parameters: CreateValidationSetParameters) =\n        postServer<CreateValidationSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"validation-set/create\")\n\n    /// Gets a validation set by id.\n    static member public Get(parameters: GetValidationSetParameters) =\n        postServer<GetValidationSetParameters, ValidationSetDto> (parameters |> ensureCorrelationIdIsSet, \"validation-set/get\")\n\n    /// Updates a validation set.\n    static member public Update(parameters: UpdateValidationSetParameters) =\n        postServer<UpdateValidationSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"validation-set/update\")\n\n    /// Logically deletes a validation set.\n    static member public Delete(parameters: DeleteValidationSetParameters) =\n        postServer<DeleteValidationSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"validation-set/delete\")\n"
  },
  {
    "path": "src/Grace.SDK/WorkItem.SDK.fs",
    "content": "namespace Grace.SDK\n\nopen Grace.SDK.Common\nopen Grace.Shared.Parameters.WorkItem\nopen Grace.Types.WorkItem\nopen System.Threading.Tasks\n\n/// The WorkItem module provides a set of functions for interacting with work items in the Grace API.\ntype WorkItem() =\n    /// Creates a new work item.\n    static member public Create(parameters: CreateWorkItemParameters) =\n        postServer<CreateWorkItemParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/create\")\n\n    /// Gets a work item by ID.\n    static member public Get(parameters: GetWorkItemParameters) =\n        postServer<GetWorkItemParameters, WorkItemDto> (parameters |> ensureCorrelationIdIsSet, \"work/get\")\n\n    /// Updates a work item.\n    static member public Update(parameters: UpdateWorkItemParameters) =\n        postServer<UpdateWorkItemParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/update\")\n\n    /// Links a reference to a work item.\n    static member public LinkReference(parameters: LinkReferenceParameters) =\n        postServer<LinkReferenceParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/link/reference\")\n\n    /// Links an artifact to a work item.\n    static member public LinkArtifact(parameters: LinkArtifactParameters) =\n        postServer<LinkArtifactParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/link/artifact\")\n\n    /// Adds summary content (and optional prompt content) to a work item using the canonical add-summary contract.\n    static member public AddSummary(parameters: AddSummaryParameters) =\n        postServer<AddSummaryParameters, AddSummaryResult> (parameters |> ensureCorrelationIdIsSet, \"work/add-summary\")\n\n    /// Links a promotion set to a work item.\n    static member public LinkPromotionSet(parameters: LinkPromotionSetParameters) =\n        postServer<LinkPromotionSetParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/link/promotion-set\")\n\n    /// Gets categorized links for a work item.\n    static member public GetLinks(parameters: GetWorkItemLinksParameters) =\n        postServer<GetWorkItemLinksParameters, WorkItemLinksDto> (parameters |> ensureCorrelationIdIsSet, \"work/links/list\")\n\n    /// Lists reviewer attachments for a work item.\n    static member public ListAttachments(parameters: ListWorkItemAttachmentsParameters) =\n        postServer<ListWorkItemAttachmentsParameters, ListWorkItemAttachmentsResult> (parameters |> ensureCorrelationIdIsSet, \"work/attachments/list\")\n\n    /// Shows a reviewer attachment for a work item using deterministic selection semantics.\n    static member public ShowAttachment(parameters: ShowWorkItemAttachmentParameters) =\n        postServer<ShowWorkItemAttachmentParameters, ShowWorkItemAttachmentResult> (parameters |> ensureCorrelationIdIsSet, \"work/attachments/show\")\n\n    /// Gets attachment download metadata for a linked reviewer attachment.\n    static member public DownloadAttachment(parameters: DownloadWorkItemAttachmentParameters) =\n        postServer<DownloadWorkItemAttachmentParameters, DownloadWorkItemAttachmentResult> (parameters |> ensureCorrelationIdIsSet, \"work/attachments/download\")\n\n    /// Removes a reference link from a work item.\n    static member public RemoveReferenceLink(parameters: RemoveReferenceLinkParameters) =\n        postServer<RemoveReferenceLinkParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/links/remove/reference\")\n\n    /// Removes a promotion-set link from a work item.\n    static member public RemovePromotionSetLink(parameters: RemovePromotionSetLinkParameters) =\n        postServer<RemovePromotionSetLinkParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/links/remove/promotion-set\")\n\n    /// Removes an artifact link from a work item.\n    static member public RemoveArtifactLink(parameters: RemoveArtifactLinkParameters) =\n        postServer<RemoveArtifactLinkParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/links/remove/artifact\")\n\n    /// Removes all artifact links of a specific artifact type from a work item.\n    static member public RemoveArtifactTypeLinks(parameters: RemoveArtifactTypeLinksParameters) =\n        postServer<RemoveArtifactTypeLinksParameters, string> (parameters |> ensureCorrelationIdIsSet, \"work/links/remove/artifact-type\")\n"
  },
  {
    "path": "src/Grace.Server/AGENTS.md",
    "content": "# Grace.Server Agents Guide\n\nGlobal policies live in `../AGENTS.md`. Review them before touching server\ncode.\n\n## Purpose\n\n- Host the Orleans cluster, expose HTTP endpoints via Giraffe, and orchestrate\n  startup and configuration for the Grace backend.\n- Provide the primary HTTP surface area consumed by the CLI, SDK, and other\n  integrations.\n\n## Key Patterns\n\n- Use the `task { }` computation expression for async flows. Keep HTTP handlers\n  small and delegate complex logic to separate modules.\n- Follow the Giraffe `HttpHandler` style (`fun next ctx -> task { ... }`) and\n  guard handler bodies with defensive `try/with` blocks when needed.\n- Preserve structured logging (including correlation IDs) and ensure middleware\n  ordering remains stable.\n- Test bootstrap: set `grace__auth__bootstrap__system_admin_user_id` to seed a\n  SystemAdmin assignment at system scope for integration tests.\n- Keep configuration loading and Orleans startup sequencing intact. Changes\n  should be intentional and well documented here.\n- Coordinate contracts and message flows with `Grace.Actors`, `Grace.SDK`, and\n  `Grace.Types` so that clients and grain logic remain in sync.\n- Personal access tokens (PATs) use the `GracePat` auth scheme, `/auth/token/*`\n  endpoints, and are governed by `grace__auth__pat__*` lifetime policies.\n  Authorization headers are redacted in request logging.\n- Auth0 JWT bearer validation is configured via\n  `grace__auth__oidc__authority` and `grace__auth__oidc__audience`.\n  Server-hosted interactive login is disabled in this phase.\n\n## Project Rules\n\n1. When modifying `Program.Server.fs`, `OrleansConfig.fs`, or startup modules,\n   verify that ordering, options binding, and health checks remain correct.\n2. Add route-level tests (and, when practical, in-memory Orleans integration\n   tests) for new behaviors or regressions you fix.\n3. Note any unusual hosting assumptions or deployment considerations here so\n   agents avoid unnecessary spelunking.\n\n## Validation\n\n- Update or add tests covering new endpoints or handlers and run\n  `dotnet test --no-build`.\n- Rebuild the solution with `dotnet build --configuration Release` to catch\n  dependency or configuration issues early.\n- Consider running targeted integration smoke tests when touching Orleans\n  clustering or storage configuration.\n"
  },
  {
    "path": "src/Grace.Server/Access.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Extensions\nopen Grace.Server.Security\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Parameters.Access\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule Access =\n\n    let private requireGraceUser (handler: HttpHandler) : HttpHandler =\n        fun next context ->\n            match PrincipalMapper.tryGetUserId context.User with\n            | Some _ -> handler next context\n            | None -> RequestErrors.UNAUTHORIZED \"Grace\" \"Access\" \"Authentication required.\" next context\n\n    let private includeReason = Environment.GetEnvironmentVariable(\"GRACE_TESTING\") = \"1\"\n\n    let private forbiddenResult (context: HttpContext) (reason: string) =\n        let message =\n            if\n                includeReason\n                && not (String.IsNullOrWhiteSpace reason)\n            then\n                reason\n            else\n                \"Forbidden.\"\n\n        task {\n            context.Response.StatusCode <- StatusCodes.Status403Forbidden\n            do! context.Response.WriteAsync(message)\n            return Some context\n        }\n\n    let private scopeKind (scope: Scope) =\n        match scope with\n        | Scope.System -> \"system\"\n        | Scope.Owner _ -> \"owner\"\n        | Scope.Organization _ -> \"organization\"\n        | Scope.Repository _ -> \"repository\"\n        | Scope.Branch _ -> \"branch\"\n\n    let private resourceForScope (scope: Scope) =\n        match scope with\n        | Scope.System -> Resource.System\n        | Scope.Owner ownerId -> Resource.Owner ownerId\n        | Scope.Organization (ownerId, organizationId) -> Resource.Organization(ownerId, organizationId)\n        | Scope.Repository (ownerId, organizationId, repositoryId) -> Resource.Repository(ownerId, organizationId, repositoryId)\n        | Scope.Branch (ownerId, organizationId, repositoryId, branchId) -> Resource.Branch(ownerId, organizationId, repositoryId, branchId)\n\n    let private adminOperationForScope (scope: Scope) =\n        match scope with\n        | Scope.System -> Operation.SystemAdmin\n        | Scope.Owner _ -> Operation.OwnerAdmin\n        | Scope.Organization _ -> Operation.OrgAdmin\n        | Scope.Repository _ -> Operation.RepoAdmin\n        | Scope.Branch _ -> Operation.BranchAdmin\n\n    let private adminOperationForResource (resource: Resource) =\n        match resource with\n        | Resource.System -> Operation.SystemAdmin\n        | Resource.Owner _ -> Operation.OwnerAdmin\n        | Resource.Organization _ -> Operation.OrgAdmin\n        | Resource.Repository _ -> Operation.RepoAdmin\n        | Resource.Branch _ -> Operation.BranchAdmin\n        | Resource.Path (ownerId, organizationId, repositoryId, _relativePath) -> Operation.RepoAdmin\n\n    let private authorize (context: HttpContext) (operation: Operation) (resource: Resource) =\n        task {\n            let principals = PrincipalMapper.getPrincipals context.User\n            let claims = PrincipalMapper.getEffectiveClaims context.User\n            let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n            let! decision = evaluator.CheckAsync(principals, claims, operation, resource)\n\n            match decision with\n            | Allowed _ -> return Ok()\n            | Denied reason -> return Error reason\n        }\n\n    let private authorizeScopeAdmin (context: HttpContext) (scope: Scope) =\n        let operation = adminOperationForScope scope\n        let resource = resourceForScope scope\n        authorize context operation resource\n\n    let private parseGuid (value: string) (fieldName: string) (correlationId: CorrelationId) =\n        let mutable parsed = Guid.Empty\n\n        if String.IsNullOrWhiteSpace value then\n            Error(GraceError.Create $\"{fieldName} is required.\" correlationId)\n        else if Guid.TryParse(value, &parsed) then\n            Ok parsed\n        else\n            Error(GraceError.Create $\"{fieldName} must be a valid Guid.\" correlationId)\n\n    let private parsePrincipal (principalType: string) (principalId: string) (correlationId: CorrelationId) =\n        if String.IsNullOrWhiteSpace principalType\n           || String.IsNullOrWhiteSpace principalId then\n            Error(GraceError.Create \"PrincipalType and PrincipalId are required.\" correlationId)\n        else\n            match discriminatedUnionFromString<PrincipalType> principalType with\n            | Some parsed -> Ok { PrincipalType = parsed; PrincipalId = principalId }\n            | None -> Error(GraceError.Create $\"Invalid PrincipalType '{principalType}'.\" correlationId)\n\n    let private parseScope (scopeKind: string) (parameters: AccessParameters) (correlationId: CorrelationId) =\n        let normalized =\n            if String.IsNullOrWhiteSpace scopeKind then\n                String.Empty\n            else\n                scopeKind.Trim().ToLowerInvariant()\n\n        match normalized with\n        | \"\" -> Error(GraceError.Create \"ScopeKind is required.\" correlationId)\n        | \"system\" -> Ok Scope.System\n        | \"owner\" ->\n            parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId\n            |> Result.map (fun ownerId -> Scope.Owner ownerId)\n        | \"org\"\n        | \"organization\" ->\n            match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n            | Error error -> Error error\n            | Ok ownerId ->\n                parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId\n                |> Result.map (fun organizationId -> Scope.Organization(ownerId, organizationId))\n        | \"repo\"\n        | \"repository\" ->\n            match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n            | Error error -> Error error\n            | Ok ownerId ->\n                match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n                | Error error -> Error error\n                | Ok organizationId ->\n                    parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId\n                    |> Result.map (fun repositoryId -> Scope.Repository(ownerId, organizationId, repositoryId))\n        | \"branch\" ->\n            match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n            | Error error -> Error error\n            | Ok ownerId ->\n                match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n                | Error error -> Error error\n                | Ok organizationId ->\n                    match parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId with\n                    | Error error -> Error error\n                    | Ok repositoryId ->\n                        parseGuid parameters.BranchId (nameof parameters.BranchId) correlationId\n                        |> Result.map (fun branchId -> Scope.Branch(ownerId, organizationId, repositoryId, branchId))\n        | other -> Error(GraceError.Create $\"Invalid ScopeKind '{other}'.\" correlationId)\n\n    let private tryParseRepositoryResource (parameters: AccessParameters) (correlationId: CorrelationId) =\n        match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n        | Error error -> Error error\n        | Ok ownerId ->\n            match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n            | Error error -> Error error\n            | Ok organizationId ->\n                match parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId with\n                | Error error -> Error error\n                | Ok repositoryId -> Ok(Resource.Repository(ownerId, organizationId, repositoryId))\n\n    let private parseResource (resourceKind: string) (parameters: CheckPermissionParameters) (correlationId: CorrelationId) =\n        let normalized =\n            if String.IsNullOrWhiteSpace resourceKind then\n                String.Empty\n            else\n                resourceKind.Trim().ToLowerInvariant()\n\n        match normalized with\n        | \"\" -> Error(GraceError.Create \"ResourceKind is required.\" correlationId)\n        | \"system\" -> Ok Resource.System\n        | \"owner\" ->\n            parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId\n            |> Result.map (fun ownerId -> Resource.Owner ownerId)\n        | \"org\"\n        | \"organization\" ->\n            match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n            | Error error -> Error error\n            | Ok ownerId ->\n                parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId\n                |> Result.map (fun organizationId -> Resource.Organization(ownerId, organizationId))\n        | \"repo\"\n        | \"repository\" ->\n            match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n            | Error error -> Error error\n            | Ok ownerId ->\n                match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n                | Error error -> Error error\n                | Ok organizationId ->\n                    parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId\n                    |> Result.map (fun repositoryId -> Resource.Repository(ownerId, organizationId, repositoryId))\n        | \"branch\" ->\n            match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n            | Error error -> Error error\n            | Ok ownerId ->\n                match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n                | Error error -> Error error\n                | Ok organizationId ->\n                    match parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId with\n                    | Error error -> Error error\n                    | Ok repositoryId ->\n                        parseGuid parameters.BranchId (nameof parameters.BranchId) correlationId\n                        |> Result.map (fun branchId -> Resource.Branch(ownerId, organizationId, repositoryId, branchId))\n        | \"path\" ->\n            if String.IsNullOrWhiteSpace parameters.Path then\n                Error(GraceError.Create \"Path is required for Path resources.\" correlationId)\n            else\n                match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n                | Error error -> Error error\n                | Ok ownerId ->\n                    match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n                    | Error error -> Error error\n                    | Ok organizationId ->\n                        parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId\n                        |> Result.map (fun repositoryId -> Resource.Path(ownerId, organizationId, repositoryId, parameters.Path))\n        | other -> Error(GraceError.Create $\"Invalid ResourceKind '{other}'.\" correlationId)\n\n    let private parseOperation (operation: string) (correlationId: CorrelationId) =\n        if String.IsNullOrWhiteSpace operation then\n            Error(GraceError.Create \"Operation is required.\" correlationId)\n        else\n            match discriminatedUnionFromString<Operation> operation with\n            | Some parsed -> Ok parsed\n            | None -> Error(GraceError.Create $\"Invalid Operation '{operation}'.\" correlationId)\n\n    let private tryParsePrincipalFilter (principalType: string) (principalId: string) (correlationId: CorrelationId) =\n        if String.IsNullOrWhiteSpace principalType\n           && String.IsNullOrWhiteSpace principalId then\n            Ok None\n        elif String.IsNullOrWhiteSpace principalType\n             || String.IsNullOrWhiteSpace principalId then\n            Error(GraceError.Create \"PrincipalType and PrincipalId must be provided together.\" correlationId)\n        else\n            parsePrincipal principalType principalId correlationId\n            |> Result.map Some\n\n    let GrantRole: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<GrantRoleParameters>\n                let correlationId = parameters.CorrelationId\n\n                let validationResult =\n                    match parseScope parameters.ScopeKind parameters correlationId with\n                    | Error error -> Error error\n                    | Ok scope ->\n                        match parsePrincipal parameters.PrincipalType parameters.PrincipalId correlationId with\n                        | Error error -> Error error\n                        | Ok principal ->\n                            if String.IsNullOrWhiteSpace parameters.RoleId then\n                                Error(GraceError.Create \"RoleId is required.\" correlationId)\n                            else\n                                match Authorization.RoleCatalog.tryGet parameters.RoleId with\n                                | None -> Error(GraceError.Create $\"Unknown RoleId '{parameters.RoleId}'.\" correlationId)\n                                | Some roleDefinition ->\n                                    if\n                                        not\n                                        <| roleDefinition.AppliesTo.Contains(scopeKind scope)\n                                    then\n                                        Error(GraceError.Create $\"Role '{parameters.RoleId}' does not apply to scope '{scopeKind scope}'.\" correlationId)\n                                    else\n                                        Ok(scope, principal)\n\n                match validationResult with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok (scope, principal) ->\n                    let! authorizationResult = authorizeScopeAdmin context scope\n\n                    match authorizationResult with\n                    | Error reason -> return! forbiddenResult context reason\n                    | Ok _ ->\n                        let assignment =\n                            {\n                                Principal = principal\n                                Scope = scope\n                                RoleId = parameters.RoleId\n                                Source =\n                                    if String.IsNullOrWhiteSpace parameters.Source then\n                                        \"manual\"\n                                    else\n                                        parameters.Source\n                                SourceDetail =\n                                    if String.IsNullOrWhiteSpace parameters.SourceDetail then\n                                        None\n                                    else\n                                        Some parameters.SourceDetail\n                                CreatedAt = getCurrentInstant ()\n                            }\n\n                        let scopeKey = AccessControl.getScopeKey scope\n                        let actorProxy = ActorProxy.AccessControl.CreateActorProxy scopeKey correlationId\n\n                        match! actorProxy.Handle (AccessControlCommand.GrantRole assignment) (createMetadata context) with\n                        | Ok returnValue -> return! context |> result200Ok returnValue\n                        | Error error -> return! context |> result400BadRequest error\n            })\n\n    let RevokeRole: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<RevokeRoleParameters>\n                let correlationId = parameters.CorrelationId\n\n                match parseScope parameters.ScopeKind parameters correlationId with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok scope ->\n                    match parsePrincipal parameters.PrincipalType parameters.PrincipalId correlationId with\n                    | Error error -> return! context |> result400BadRequest error\n                    | Ok principal ->\n                        if String.IsNullOrWhiteSpace parameters.RoleId then\n                            return!\n                                context\n                                |> result400BadRequest (GraceError.Create \"RoleId is required.\" correlationId)\n                        else\n                            let! authorizationResult = authorizeScopeAdmin context scope\n\n                            match authorizationResult with\n                            | Error reason -> return! forbiddenResult context reason\n                            | Ok _ ->\n                                let scopeKey = AccessControl.getScopeKey scope\n                                let actorProxy = ActorProxy.AccessControl.CreateActorProxy scopeKey correlationId\n\n                                match! actorProxy.Handle (AccessControlCommand.RevokeRole(principal, parameters.RoleId)) (createMetadata context) with\n                                | Ok returnValue -> return! context |> result200Ok returnValue\n                                | Error error -> return! context |> result400BadRequest error\n            })\n\n    let ListRoleAssignments: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<ListRoleAssignmentsParameters>\n                let correlationId = parameters.CorrelationId\n\n                match parseScope parameters.ScopeKind parameters correlationId with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok scope ->\n                    match tryParsePrincipalFilter parameters.PrincipalType parameters.PrincipalId correlationId with\n                    | Error error -> return! context |> result400BadRequest error\n                    | Ok principalFilter ->\n                        let! authorizationResult = authorizeScopeAdmin context scope\n\n                        match authorizationResult with\n                        | Error reason -> return! forbiddenResult context reason\n                        | Ok _ ->\n                            let scopeKey = AccessControl.getScopeKey scope\n                            let actorProxy = ActorProxy.AccessControl.CreateActorProxy scopeKey correlationId\n\n                            match! actorProxy.Handle (AccessControlCommand.ListAssignments principalFilter) (createMetadata context) with\n                            | Ok returnValue -> return! context |> result200Ok returnValue\n                            | Error error -> return! context |> result400BadRequest error\n            })\n\n    let private parseClaimPermissions (claimPermissions: IList<ClaimPermissionParameters>) (correlationId: string) : Result<List<ClaimPermission>, GraceError> =\n        if isNull claimPermissions\n           || claimPermissions.Count = 0 then\n            Error(GraceError.Create \"ClaimPermissions are required.\" correlationId)\n        else\n            let folder (state: Result<List<ClaimPermission>, GraceError>) (permission: ClaimPermissionParameters) =\n                match state with\n                | Error _ -> state\n                | Ok permissions ->\n                    if String.IsNullOrWhiteSpace permission.Claim then\n                        Error(GraceError.Create \"Claim is required.\" correlationId)\n                    else\n                        match discriminatedUnionFromString<DirectoryPermission> permission.DirectoryPermission with\n                        | None -> Error(GraceError.Create $\"Invalid DirectoryPermission '{permission.DirectoryPermission}'.\" correlationId)\n                        | Some parsed ->\n                            permissions.Add({ Claim = permission.Claim; DirectoryPermission = parsed })\n                            Ok permissions\n\n            claimPermissions\n            |> Seq.fold folder (Ok(List<ClaimPermission>()))\n\n    let private tryBuildUpsertPathPermission (parameters: UpsertPathPermissionParameters) (correlationId: string) =\n        match parseGuid parameters.OwnerId (nameof parameters.OwnerId) correlationId with\n        | Error error -> Error error\n        | Ok _ ->\n            match parseGuid parameters.OrganizationId (nameof parameters.OrganizationId) correlationId with\n            | Error error -> Error error\n            | Ok _ ->\n                match parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId with\n                | Error error -> Error error\n                | Ok repositoryId ->\n                    if String.IsNullOrWhiteSpace parameters.Path then\n                        Error(GraceError.Create \"Path is required.\" correlationId)\n                    else\n                        match parseClaimPermissions parameters.ClaimPermissions correlationId with\n                        | Error error -> Error error\n                        | Ok permissions -> Ok(repositoryId, { Path = parameters.Path; Permissions = permissions })\n\n    let UpsertPathPermission: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<UpsertPathPermissionParameters>\n                let correlationId = parameters.CorrelationId\n\n                match tryParseRepositoryResource parameters correlationId with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok repositoryResource ->\n                    let! authorizationResult = authorize context Operation.RepoAdmin repositoryResource\n\n                    match authorizationResult with\n                    | Error reason -> return! forbiddenResult context reason\n                    | Ok _ ->\n                        match tryBuildUpsertPathPermission parameters correlationId with\n                        | Error error -> return! context |> result400BadRequest error\n                        | Ok (repositoryId, pathPermission) ->\n                            let actorProxy = ActorProxy.RepositoryPermission.CreateActorProxy repositoryId correlationId\n\n                            let! upsertResult = actorProxy.Handle (RepositoryPermissionCommand.UpsertPathPermission pathPermission) (createMetadata context)\n\n                            match upsertResult with\n                            | Ok returnValue -> return! context |> result200Ok returnValue\n                            | Error error -> return! context |> result400BadRequest error\n            })\n\n    let RemovePathPermission: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<RemovePathPermissionParameters>\n                let correlationId = parameters.CorrelationId\n\n                match tryParseRepositoryResource parameters correlationId with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok repositoryResource ->\n                    let! authorizationResult = authorize context Operation.RepoAdmin repositoryResource\n\n                    match authorizationResult with\n                    | Error reason -> return! forbiddenResult context reason\n                    | Ok _ ->\n                        match parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId with\n                        | Error error -> return! context |> result400BadRequest error\n                        | Ok repositoryId ->\n                            if String.IsNullOrWhiteSpace parameters.Path then\n                                return!\n                                    context\n                                    |> result400BadRequest (GraceError.Create \"Path is required.\" correlationId)\n                            else\n                                let actorProxy = ActorProxy.RepositoryPermission.CreateActorProxy repositoryId correlationId\n\n                                match! actorProxy.Handle (RepositoryPermissionCommand.RemovePathPermission parameters.Path) (createMetadata context) with\n                                | Ok returnValue -> return! context |> result200Ok returnValue\n                                | Error error -> return! context |> result400BadRequest error\n            })\n\n    let ListPathPermissions: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<ListPathPermissionsParameters>\n                let correlationId = parameters.CorrelationId\n\n                match tryParseRepositoryResource parameters correlationId with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok repositoryResource ->\n                    let! authorizationResult = authorize context Operation.RepoAdmin repositoryResource\n\n                    match authorizationResult with\n                    | Error reason -> return! forbiddenResult context reason\n                    | Ok _ ->\n                        match parseGuid parameters.RepositoryId (nameof parameters.RepositoryId) correlationId with\n                        | Error error -> return! context |> result400BadRequest error\n                        | Ok repositoryId ->\n                            let pathFilter = if String.IsNullOrWhiteSpace parameters.Path then None else Some parameters.Path\n\n                            let actorProxy = ActorProxy.RepositoryPermission.CreateActorProxy repositoryId correlationId\n\n                            match! actorProxy.Handle (RepositoryPermissionCommand.ListPathPermissions pathFilter) (createMetadata context) with\n                            | Ok returnValue -> return! context |> result200Ok returnValue\n                            | Error error -> return! context |> result400BadRequest error\n            })\n\n    let CheckPermission: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let! parameters = context |> parse<CheckPermissionParameters>\n                let correlationId = parameters.CorrelationId\n\n                match parseOperation parameters.Operation correlationId with\n                | Error error -> return! context |> result400BadRequest error\n                | Ok operation ->\n                    match parseResource parameters.ResourceKind parameters correlationId with\n                    | Error error -> return! context |> result400BadRequest error\n                    | Ok resource ->\n                        let principalFilter = tryParsePrincipalFilter parameters.PrincipalType parameters.PrincipalId correlationId\n\n                        match principalFilter with\n                        | Error error -> return! context |> result400BadRequest error\n                        | Ok principalOption ->\n                            let principals = PrincipalMapper.getPrincipals context.User\n\n                            let isCallerPrincipal principalToCheck =\n                                principals\n                                |> List.exists (fun principal -> principal = principalToCheck)\n\n                            let! allowCheck =\n                                match principalOption with\n                                | None -> Task.FromResult(Ok())\n                                | Some principal when isCallerPrincipal principal -> Task.FromResult(Ok())\n                                | Some _ -> authorize context (adminOperationForResource resource) resource\n\n                            match allowCheck with\n                            | Error reason -> return! forbiddenResult context reason\n                            | Ok _ ->\n                                let principalSet, effectiveClaims =\n                                    match principalOption with\n                                    | Some principal -> [ principal ], Set.empty\n                                    | None ->\n                                        let claims = PrincipalMapper.getEffectiveClaims context.User\n                                        principals, claims\n\n                                let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n                                let! decision = evaluator.CheckAsync(principalSet, effectiveClaims, operation, resource)\n\n                                let returnValue = GraceReturnValue.Create decision correlationId\n                                return! context |> result200Ok returnValue\n            })\n\n    let ListRoles: HttpHandler =\n        requireGraceUser (fun next context ->\n            task {\n                let correlationId = getCorrelationId context\n                let roles = Authorization.RoleCatalog.getAll ()\n                let returnValue = GraceReturnValue.Create roles correlationId\n                return! context |> result200Ok returnValue\n            })\n"
  },
  {
    "path": "src/Grace.Server/ApplicationContext.Server.fs",
    "content": "namespace Grace.Server\n\nopen Azure.Core\nopen Azure.Identity\nopen Azure.Storage\nopen Azure.Storage.Blobs\nopen Grace.Actors.Constants\nopen Grace.Actors.Context\nopen Grace.Actors.Types\nopen Grace.Shared\nopen Grace.Shared.AzureEnvironment\nopen Grace.Shared.Constants\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.Extensions.Configuration\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.ObjectPool\nopen NodaTime\nopen Orleans\nopen Orleans.Runtime\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Globalization\nopen System.IO\nopen System.Linq\nopen System.Net.Http\nopen System.Net.Sockets\nopen System.Reflection\nopen System.Text\nopen System.Threading\nopen System.Threading.Tasks\nopen System.Diagnostics\nopen Microsoft.AspNetCore.Hosting\nopen Orleans.Serialization\nopen System.Net.Security\nopen Microsoft.AspNetCore.SignalR\n\nmodule ApplicationContext =\n\n    let mutable private configuration: IConfiguration = null\n    let Configuration () : IConfiguration = configuration\n    let mutable private log: ILogger = null\n\n    /// Global dictionary of timing information for each request.\n    let timings = ConcurrentDictionary<CorrelationId, List<Timing>>()\n\n    /// Orleans client instance for the application.\n    let mutable grainFactory: IGrainFactory = null\n    //let orleansClient = ServiceCollection().FirstOrDefault(fun service -> service.ServiceType = typeof<IGrainFactory>).ImplementationInstance :?> IGrainFactory\n\n    let mutable serviceProvider: IServiceProvider = null\n\n    /// Actor state storage provider instance\n    let mutable actorStateStorageProvider: ActorStateStorageProvider = ActorStateStorageProvider.Unknown\n\n    /// Logger factory instance\n    let mutable loggerFactory: ILoggerFactory = null\n\n    /// Grace Server's universal .NET memory cache\n    let mutable memoryCache: IMemoryCache = null\n\n    /// CosmosDB container instance (set during startup).\n    let mutable cosmosContainer: Container = null\n\n    /// Pub-sub settings for the application.\n    let mutable pubSubSettings: GracePubSubSettings = GracePubSubSettings.Empty\n\n    let private defaultAzureCredential = lazy (DefaultAzureCredential())\n\n    /// Sets the Application global configuration.\n    let setConfiguration (config: IConfiguration) =\n        let assembly = Assembly.GetExecutingAssembly()\n        let version = assembly.GetName().Version\n        let fileInfo = FileInfo(assembly.Location)\n\n        logToConsole $\"Grace Server version: {version}; build time (UTC): {fileInfo.LastWriteTimeUtc}.\"\n\n        configuration <- config\n    //configuration.AsEnumerable() |> Seq.iter (fun kvp -> logToConsole $\"{kvp.Key}: {kvp.Value}\")\n\n    /// Sets the Orleans client (IGrainFactory instance) for the application.\n    let setOrleansClient client =\n        grainFactory <- client\n        setOrleansClient grainFactory\n\n    /// Sets the ActorStateStorageProvider for the application.\n    let setActorStateStorageProvider actorStateStorage =\n        actorStateStorageProvider <- actorStateStorage\n        logToConsole $\"In ApplicationContext.Server.setActorStateStorageProvider: Setting actor state storage provider to {actorStateStorageProvider}.\"\n        setActorStateStorageProvider actorStateStorageProvider\n\n    /// Sets the ILoggerFactory for the application.\n    let setLoggerFactory logFactory =\n        loggerFactory <- logFactory\n        log <- loggerFactory.CreateLogger(\"ApplicationContext.Server\")\n        setLoggerFactory loggerFactory\n\n    let setPubSubSettings settings =\n        pubSubSettings <- settings\n        setPubSubSettings settings\n\n    /// Holds information about each Azure Storage Account used by the application.\n    type StorageAccount = { StorageAccountName: string; StorageAccountConnectionString: string }\n\n    let mutable sharedKeyCredential: StorageSharedKeyCredential = null\n    let mutable grpcPortListener: TcpListener = null\n    let secondsToDelayReminderProcessing = 30.0\n\n    let useManagedIdentity = AzureEnvironment.useManagedIdentity\n\n    if not useManagedIdentity then\n        let storageKey = Environment.GetEnvironmentVariable(EnvironmentVariables.AzureStorageKey)\n        sharedKeyCredential <- StorageSharedKeyCredential(DefaultObjectStorageAccount, storageKey)\n\n    let defaultObjectStorageProvider = ObjectStorageProvider.AzureBlobStorage\n\n    let cosmosClientOptions =\n        CosmosClientOptions(ApplicationName = \"Grace.Server\", LimitToEndpoint = false, UseSystemTextJsonSerializerWithOptions = Constants.JsonSerializerOptions)\n\n    let private tryGetConfigValue (config: IConfiguration) (name: string) =\n        if isNull config then\n            None\n        else\n            let value = config[getConfigKey name]\n            if String.IsNullOrWhiteSpace value then None else Some(value.Trim())\n\n    let private tryGetCosmosConnectionString (config: IConfiguration) = tryGetConfigValue config EnvironmentVariables.AzureCosmosDBConnectionString\n\n    let isGraceTesting =\n        match Environment.GetEnvironmentVariable(\"GRACE_TESTING\") with\n        | null -> false\n        | value ->\n            value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n            || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n\n    let isLocalDebugEnvironment =\n        match Environment.GetEnvironmentVariable EnvironmentVariables.DebugEnvironment with\n        | null -> false\n        | value -> value.Equals(\"Local\", StringComparison.OrdinalIgnoreCase)\n\n    let private isLocalEndpoint (config: IConfiguration) =\n        match tryGetCosmosConnectionString config with\n        | Some value ->\n            value.Contains(\"localhost\", StringComparison.OrdinalIgnoreCase)\n            || value.Contains(\"127.0.0.1\", StringComparison.OrdinalIgnoreCase)\n        | None -> false\n\n    let private applyLocalEmulatorSettings (config: IConfiguration) =\n        let useLocalEmulatorSettings =\n            not useManagedIdentity\n            && (isGraceTesting\n                || isLocalDebugEnvironment\n                || isLocalEndpoint config)\n\n        if useLocalEmulatorSettings then\n            // The CosmosDB emulator uses a self-signed certificate, and, by default, HttpClient will refuse\n            //   to connect over https: if the certificate can't be traced back to a root.\n            // These settings allow Grace Server to access the CosmosDB Emulator by bypassing TLS.\n            let handler =\n                new SocketsHttpHandler(\n                    SslOptions =\n                        new SslClientAuthenticationOptions(\n                            TargetHost = \"localhost\", // SNI host_name must be DNS per RFC 6066\n                            RemoteCertificateValidationCallback = (fun _ __ ___ ____ -> true) // emulator only\n                        )\n                )\n\n            cosmosClientOptions.HttpClientFactory <- (fun () -> new HttpClient(handler, disposeHandler = true))\n\n            // During debugging, we might want to see the responses.\n            cosmosClientOptions.EnableContentResponseOnWrite <- true\n\n            // When using the CosmosDB Emulator, these settings help with connectivity.\n            cosmosClientOptions.LimitToEndpoint <- true\n            cosmosClientOptions.ConnectionMode <- ConnectionMode.Gateway\n\n    let private resolveSetting (config: IConfiguration) (name: string) =\n        match tryGetConfigValue config name with\n        | Some value -> value\n        | None -> invalidOp $\"Configuration value '{getConfigKey name}' must be set.\"\n\n    /// Sets multiple values for the application. In functional programming, a global construct like this is used instead of dependency injection.\n    let configurePubSubSettings () =\n        let rawSystem = Environment.GetEnvironmentVariable EnvironmentVariables.GracePubSubSystem\n\n        let system =\n            match rawSystem with\n            | value when value.Equals(\"AzureEventHubs\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.AzureEventHubs\n            | value when value.Equals(\"AzureServiceBus\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.AzureServiceBus\n            | value when value.Equals(\"AWS_SQS\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.AwsSqs\n            | value when value.Equals(\"AWS-SQS\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.AwsSqs\n            | value when value.Equals(\"AWS\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.AwsSqs\n            | value when value.Equals(\"AWS SQS\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.AwsSqs\n            | value when value.Equals(\"GCP\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.GoogleCloudPubSub\n            | value when value.Equals(\"GOOGLE_CLOUD_PUBSUB\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.GoogleCloudPubSub\n            | value when value.Equals(\"GOOGLECLOUDPUBSUB\", StringComparison.OrdinalIgnoreCase) -> GracePubSubSystem.GoogleCloudPubSub\n            | _ -> GracePubSubSystem.UnknownPubSubProvider\n\n        let azureServiceBusSettings =\n            if system = GracePubSubSystem.AzureServiceBus then\n                let serviceBusConnectionString = Environment.GetEnvironmentVariable EnvironmentVariables.AzureServiceBusConnectionString\n\n                if not\n                   <| AzureEnvironment.useManagedIdentityForServiceBus then\n                    if String.IsNullOrWhiteSpace(serviceBusConnectionString) then\n                        invalidOp\n                            $\"Environment variable '{EnvironmentVariables.AzureServiceBusConnectionString}' must be set when {EnvironmentVariables.GracePubSubSystem} is {GracePubSubSystem.AzureServiceBus} and you're not using a managed identity.\"\n\n                let sb_namespace = Environment.GetEnvironmentVariable EnvironmentVariables.AzureServiceBusNamespace\n\n                if String.IsNullOrWhiteSpace(sb_namespace) then\n                    invalidOp\n                        $\"Environment variable '{EnvironmentVariables.AzureServiceBusNamespace}' must be set when {EnvironmentVariables.GracePubSubSystem} is {GracePubSubSystem.AzureServiceBus}.\"\n\n                let topic = Environment.GetEnvironmentVariable EnvironmentVariables.AzureServiceBusTopic\n\n                if String.IsNullOrWhiteSpace(topic) then\n                    invalidOp\n                        $\"Environment variable '{EnvironmentVariables.AzureServiceBusTopic}' must be set when {EnvironmentVariables.GracePubSubSystem} is {GracePubSubSystem.AzureServiceBus}.\"\n\n                let subscription = Environment.GetEnvironmentVariable EnvironmentVariables.AzureServiceBusSubscription\n\n                if String.IsNullOrWhiteSpace(subscription) then\n                    invalidOp\n                        $\"Environment variable '{EnvironmentVariables.AzureServiceBusSubscription}' must be set when {EnvironmentVariables.GracePubSubSystem} is {GracePubSubSystem.AzureServiceBus}.\"\n\n                let fullyQualifiedNamespace =\n                    AzureEnvironment.tryGetServiceBusFullyQualifiedNamespace ()\n                    |> Option.defaultWith (fun () ->\n                        invalidOp\n                            $\"Environment variable '{EnvironmentVariables.AzureServiceBusNamespace}' must be set when {EnvironmentVariables.GracePubSubSystem} is {GracePubSubSystem.AzureServiceBus}.\")\n\n                let useManagedIdentity = AzureEnvironment.useManagedIdentityForServiceBus\n\n                Some\n                    {\n                        ConnectionString =\n                            if String.IsNullOrEmpty(serviceBusConnectionString) then\n                                String.Empty\n                            else\n                                serviceBusConnectionString\n                        FullyQualifiedNamespace = fullyQualifiedNamespace\n                        TopicName = topic\n                        SubscriptionName = subscription\n                        UseManagedIdentity = useManagedIdentity\n                    }\n            else\n                None\n\n        { System = system; AzureServiceBus = azureServiceBusSettings }\n\n    let Set () =\n        task {\n            try\n                if isNull configuration then\n                    invalidOp \"Configuration must be set before initializing ApplicationContext.\"\n\n                applyLocalEmulatorSettings configuration\n\n                let cosmosDatabaseName = resolveSetting configuration EnvironmentVariables.AzureCosmosDBDatabaseName\n                let cosmosContainerName = resolveSetting configuration EnvironmentVariables.AzureCosmosDBContainerName\n\n                let cosmosClient =\n                    if useManagedIdentity then\n                        let endpoint =\n                            AzureEnvironment.tryGetCosmosEndpointUri ()\n                            |> Option.defaultWith (fun () -> invalidOp \"Azure Cosmos DB endpoint must be configured when using a managed identity.\")\n\n                        new CosmosClient(endpoint.AbsoluteUri, defaultAzureCredential.Value, cosmosClientOptions)\n                    else\n                        let cosmosDbConnectionString = resolveSetting configuration EnvironmentVariables.AzureCosmosDBConnectionString\n                        new CosmosClient(cosmosDbConnectionString, cosmosClientOptions)\n\n                let database = cosmosClient.GetDatabase(cosmosDatabaseName)\n                cosmosContainer <- database.GetContainer(cosmosContainerName)\n\n                logToConsole $\"Using CosmosDB database '{cosmosDatabaseName}' and container '{cosmosContainer.Id}'.\"\n\n                // Inject things into Actor Services.\n                setCosmosClient cosmosClient\n                setCosmosContainer cosmosContainer\n                setTimings timings\n\n                configurePubSubSettings () |> setPubSubSettings\n\n                logToConsole $\"Grace pub-sub configured as:{Environment.NewLine}{serialize pubSubSettings}\"\n\n                logToConsole \"Grace Server is ready.\"\n            with\n            | ex -> logToConsole ($\"{ex.ToStringDemystified()}\")\n        }\n        :> Task\n"
  },
  {
    "path": "src/Grace.Server/Artifact.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Artifact\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Artifact\nopen Grace.Types.Types\nopen Microsoft.AspNetCore.Http\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Security.Cryptography\nopen System.Text\nopen System.Threading.Tasks\n\nmodule Artifact =\n    let activitySource = new ActivitySource(\"Artifact\")\n\n    let internal buildBlobPath (createdAt: Instant) (artifactId: ArtifactId) =\n        let utc = createdAt.ToDateTimeUtc()\n        $\"grace-artifacts/{utc:yyyy}/{utc:MM}/{utc:dd}/{utc:HH}/{artifactId}\"\n\n    let internal buildDeterministicBlobPath (artifactId: ArtifactId) = $\"grace-artifacts/by-id/{artifactId}\"\n\n    let internal createDeterministicArtifactId (seed: string) =\n        let normalizedSeed =\n            if String.IsNullOrWhiteSpace(seed) then\n                String.Empty\n            else\n                seed.Trim().ToLowerInvariant()\n\n        let seedBytes = Encoding.UTF8.GetBytes(normalizedSeed)\n\n        use hasher = SHA256.Create()\n        let hash = hasher.ComputeHash(seedBytes)\n        let guidBytes = hash[0..15]\n        guidBytes[6] <- (guidBytes[6] &&& 0x0Fuy) ||| 0x50uy\n        guidBytes[8] <- (guidBytes[8] &&& 0x3Fuy) ||| 0x80uy\n        Guid(guidBytes)\n\n    let internal parseArtifactType (rawArtifactType: string) =\n        if String.Equals(rawArtifactType, \"AgentSummary\", StringComparison.OrdinalIgnoreCase) then\n            ArtifactType.AgentSummary\n        elif String.Equals(rawArtifactType, \"ConflictReport\", StringComparison.OrdinalIgnoreCase) then\n            ArtifactType.ConflictReport\n        elif String.Equals(rawArtifactType, \"Prompt\", StringComparison.OrdinalIgnoreCase) then\n            ArtifactType.Prompt\n        elif String.Equals(rawArtifactType, \"ValidationOutput\", StringComparison.OrdinalIgnoreCase) then\n            ArtifactType.ValidationOutput\n        elif String.Equals(rawArtifactType, \"ReviewNotes\", StringComparison.OrdinalIgnoreCase) then\n            ArtifactType.ReviewNotes\n        elif String.Equals(rawArtifactType, \"Other\", StringComparison.OrdinalIgnoreCase) then\n            ArtifactType.Other \"Other\"\n        else\n            ArtifactType.Other rawArtifactType\n\n    let private getPrincipal (context: HttpContext) =\n        if\n            isNull context.User\n            || isNull context.User.Identity\n            || String.IsNullOrWhiteSpace(context.User.Identity.Name)\n        then\n            Grace.Shared.Constants.GraceSystemUser\n        else\n            context.User.Identity.Name\n\n    let private parseGuidQueryParameter (context: HttpContext) (queryParameterName: string) (error: ArtifactError) =\n        match context.TryGetQueryStringValue queryParameterName with\n        | Some rawValue when not (String.IsNullOrWhiteSpace rawValue) ->\n            let mutable parsed = Guid.Empty\n\n            if\n                Guid.TryParse(rawValue, &parsed)\n                && parsed <> Guid.Empty\n            then\n                Ok parsed\n            else\n                Error error\n        | _ -> Error error\n\n    /// Creates artifact metadata and returns upload uri details.\n    let Create: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"Create\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<CreateArtifactParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations =\n                    [|\n                        (if String.IsNullOrWhiteSpace(parameters.ArtifactId) then\n                             Ok() |> returnValueTask\n                         else\n                             Guid.isValidAndNotEmptyGuid parameters.ArtifactId ArtifactError.InvalidArtifactId)\n                        String.isNotEmpty parameters.ArtifactType ArtifactError.InvalidArtifactType\n                        String.isNotEmpty parameters.MimeType ArtifactError.InvalidMimeType\n                        if parameters.Size >= 0L then Ok() else Error ArtifactError.InvalidSize\n                        |> returnValueTask\n                    |]\n\n                let! validationsPassed = validations |> allPass\n\n                if validationsPassed then\n                    let artifactId =\n                        if String.IsNullOrWhiteSpace(parameters.ArtifactId) then\n                            Guid.NewGuid()\n                        else\n                            Guid.Parse(parameters.ArtifactId)\n\n                    let createdAt = getCurrentInstant ()\n                    let blobPath = buildBlobPath createdAt artifactId\n                    let artifactType = parseArtifactType parameters.ArtifactType\n                    let repositoryActorProxy = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n                    let! repositoryDto = repositoryActorProxy.Get correlationId\n                    let! uploadUri = getUriWithWriteSharedAccessSignature repositoryDto blobPath correlationId\n\n                    let artifactDto: ArtifactMetadata =\n                        { ArtifactMetadata.Default with\n                            ArtifactId = artifactId\n                            OwnerId = graceIds.OwnerId\n                            OrganizationId = graceIds.OrganizationId\n                            RepositoryId = graceIds.RepositoryId\n                            ArtifactType = artifactType\n                            MimeType = parameters.MimeType\n                            Size = parameters.Size\n                            Sha256 =\n                                if String.IsNullOrWhiteSpace(parameters.Sha256) then\n                                    None\n                                else\n                                    Some(Sha256Hash parameters.Sha256)\n                            BlobPath = blobPath\n                            CreatedAt = createdAt\n                            CreatedBy = UserId(getPrincipal context)\n                        }\n\n                    let metadata = createMetadata context\n                    let artifactActorProxy = Artifact.CreateActorProxy artifactId graceIds.RepositoryId correlationId\n\n                    match! artifactActorProxy.Handle (ArtifactCommand.Create artifactDto) metadata with\n                    | Error graceError -> return! context |> result400BadRequest graceError\n                    | Ok _ ->\n                        let response: ArtifactCreateResult = { ArtifactId = artifactId; UploadUri = uploadUri; BlobPath = blobPath }\n\n                        let graceReturnValue =\n                            (GraceReturnValue.Create response correlationId)\n                                .enhance(getParametersAsDictionary parameters)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof ArtifactId, artifactId)\n                                .enhance (\"Path\", context.Request.Path.Value)\n\n                        return! context |> result200Ok graceReturnValue\n                else\n                    let! validationError = validations |> getFirstError\n                    let errorMessage = ArtifactError.getErrorMessage (validationError: ArtifactError option)\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create errorMessage correlationId)\n            }\n\n    /// Gets a read uri for an artifact.\n    let GetDownloadUri (artifactId: Guid) : HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"GetDownloadUri\", ActivityKind.Server)\n                let correlationId = getCorrelationId context\n\n                match parseGuidQueryParameter context \"organizationId\" ArtifactError.InvalidArtifactId with\n                | Error error ->\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create (ArtifactError.getErrorMessage error) correlationId)\n                | Ok organizationId ->\n                    match parseGuidQueryParameter context \"repositoryId\" ArtifactError.InvalidArtifactId with\n                    | Error error ->\n                        return!\n                            context\n                            |> result400BadRequest (GraceError.Create (ArtifactError.getErrorMessage error) correlationId)\n                    | Ok repositoryId ->\n                        let artifactActorProxy = Artifact.CreateActorProxy artifactId repositoryId correlationId\n\n                        match! artifactActorProxy.Get correlationId with\n                        | None ->\n                            return!\n                                context\n                                |> result400BadRequest (GraceError.Create (ArtifactError.getErrorMessage ArtifactError.ArtifactDoesNotExist) correlationId)\n                        | Some artifact ->\n                            let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryId correlationId\n                            let! repositoryDto = repositoryActorProxy.Get correlationId\n                            let! downloadUri = getUriWithReadSharedAccessSignature repositoryDto artifact.BlobPath correlationId\n\n                            let response: ArtifactDownloadUriResult = { ArtifactId = artifactId; DownloadUri = downloadUri }\n\n                            let graceReturnValue = GraceReturnValue.Create response correlationId\n                            return! context |> result200Ok graceReturnValue\n            }\n"
  },
  {
    "path": "src/Grace.Server/Auth.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Server.Security\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Parameters.Auth\nopen Grace.Shared.Utilities\nopen Grace.Types.Auth\nopen Grace.Types.PersonalAccessToken\nopen Grace.Types.Types\nopen Microsoft.AspNetCore.Authentication\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Configuration\nopen NodaTime\nopen System\nopen System.Text\n\nmodule Auth =\n\n    type AuthInfo = { GraceUserId: string; Claims: string list; RawClaims: (string * string) list }\n\n    let private isTesting () =\n        match Environment.GetEnvironmentVariable(\"GRACE_TESTING\") with\n        | null -> false\n        | value ->\n            value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n            || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n            || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase)\n\n    let private tryGetQueryValue (context: HttpContext) (name: string) =\n        let values = context.Request.Query[name]\n        if values.Count = 0 then None else Some(values.ToString())\n\n    let private getReturnUrl (context: HttpContext) =\n        match tryGetQueryValue context \"returnUrl\" with\n        | Some value when not (String.IsNullOrWhiteSpace value) -> value\n        | _ -> \"/\"\n\n    let private renderLoginPage (returnUrl: string) =\n        let builder = StringBuilder()\n        builder.AppendLine(\"<!doctype html>\") |> ignore\n        builder.AppendLine(\"<html lang=\\\"en\\\">\") |> ignore\n\n        builder.AppendLine(\"<head><meta charset=\\\"utf-8\\\" /><title>Grace Login</title></head>\")\n        |> ignore\n\n        builder.AppendLine(\"<body>\") |> ignore\n\n        builder.AppendLine(\"<h1>Sign in to Grace</h1>\")\n        |> ignore\n\n        builder.AppendLine(\n            \"<p>Interactive browser login is not available on the server in this phase. Use the CLI (grace auth login) or provide GRACE_TOKEN / Auth0 M2M credentials.</p>\"\n        )\n        |> ignore\n\n        builder.AppendLine(\"</body></html>\") |> ignore\n        builder.ToString()\n\n    let Login: HttpHandler =\n        fun next context ->\n            task {\n                let returnUrl = getReturnUrl context\n                let html = renderLoginPage returnUrl\n                return! htmlString html next context\n            }\n\n    let LoginProvider (providerId: string) : HttpHandler =\n        fun next context -> task { return! RequestErrors.NOT_FOUND \"Login provider not available.\" next context }\n\n    let Logout: HttpHandler =\n        fun next context ->\n            task {\n                let returnUrl = getReturnUrl context\n                do! context.SignOutAsync()\n                return! redirectTo false returnUrl next context\n            }\n\n    let Me: HttpHandler =\n        fun next context ->\n            task {\n                match PrincipalMapper.tryGetUserId context.User with\n                | None -> return! RequestErrors.UNAUTHORIZED \"Grace\" \"Auth\" \"Authentication required.\" next context\n                | Some userId ->\n                    let claims =\n                        PrincipalMapper.getEffectiveClaims context.User\n                        |> Set.toList\n\n                    let rawClaims =\n                        context.User.Claims\n                        |> Seq.map (fun claim -> (claim.Type, claim.Value))\n                        |> Seq.sortBy (fun (claimType, claimValue) -> (claimType, claimValue))\n                        |> Seq.toList\n\n                    let info = { GraceUserId = userId; Claims = claims; RawClaims = rawClaims }\n\n                    let correlationId = getCorrelationId context\n                    let returnValue = GraceReturnValue.Create info correlationId\n                    return! context |> result200Ok returnValue\n            }\n\n    let OidcConfig (configuration: IConfiguration) : HttpHandler =\n        fun next context ->\n            task {\n                let correlationId = getCorrelationId context\n\n                match ExternalAuthConfig.tryGetOidcClientConfig configuration with\n                | Some config ->\n                    let returnValue = GraceReturnValue.Create config correlationId\n                    return! context |> result200Ok returnValue\n                | None ->\n                    let message =\n                        $\"OIDC authentication is not configured. Set {Constants.EnvironmentVariables.GraceAuthOidcAuthority}, {Constants.EnvironmentVariables.GraceAuthOidcAudience}, and {Constants.EnvironmentVariables.GraceAuthOidcCliClientId}.\"\n\n                    let error = GraceError.Create message correlationId\n                    return! context |> result400BadRequest error\n            }\n\n    let private tryGetConfigValue (configuration: IConfiguration) (name: string) =\n        if isNull configuration then\n            None\n        else\n            let value = configuration[getConfigKey name]\n            if String.IsNullOrWhiteSpace value then None else Some(value.Trim())\n\n    let private getIntConfig (configuration: IConfiguration) (name: string) (defaultValue: int) =\n        match tryGetConfigValue configuration name with\n        | None -> defaultValue\n        | Some value ->\n            match Int32.TryParse value with\n            | true, parsed when parsed > 0 -> parsed\n            | _ -> defaultValue\n\n    let private getBoolConfig (configuration: IConfiguration) (name: string) (defaultValue: bool) =\n        match tryGetConfigValue configuration name with\n        | None -> defaultValue\n        | Some value ->\n            match Boolean.TryParse value with\n            | true, parsed -> parsed\n            | _ -> defaultValue\n\n    let private getPatPolicy (configuration: IConfiguration) =\n        let defaultDays = getIntConfig configuration Constants.EnvironmentVariables.GraceAuthPatDefaultLifetimeDays 90\n\n        let maxDays = getIntConfig configuration Constants.EnvironmentVariables.GraceAuthPatMaxLifetimeDays 365\n\n        let normalizedMax = if maxDays <= 0 then 365 else maxDays\n        let normalizedDefault = if defaultDays <= 0 then 90 else defaultDays\n        let effectiveDefault = min normalizedDefault normalizedMax\n        let allowNoExpiry = getBoolConfig configuration Constants.EnvironmentVariables.GraceAuthPatAllowNoExpiry false\n\n        effectiveDefault, normalizedMax, allowNoExpiry\n\n    let private getClaimValues (context: HttpContext) (claimType: string) =\n        context.User.Claims\n        |> Seq.filter (fun claim -> claim.Type = claimType)\n        |> Seq.map (fun claim -> claim.Value)\n        |> Seq.filter (fun value -> not (String.IsNullOrWhiteSpace value))\n        |> Seq.distinct\n        |> Seq.toList\n\n    let TokenCreate (configuration: IConfiguration) : HttpHandler =\n        fun next context ->\n            task {\n                let correlationId = getCorrelationId context\n\n                match PrincipalMapper.tryGetUserId context.User with\n                | None -> return! RequestErrors.UNAUTHORIZED \"Grace\" \"Auth\" \"Authentication required.\" next context\n                | Some userId ->\n                    let! parameters = context.BindJsonAsync<CreatePersonalAccessTokenParameters>()\n                    let now = getCurrentInstant ()\n                    let defaultDays, maxDays, allowNoExpiry = getPatPolicy configuration\n                    let maxSeconds = int64 maxDays * 86400L\n\n                    let expiresAtOptionResult =\n                        if parameters.NoExpiry then\n                            if not allowNoExpiry then\n                                Error \"No-expiry tokens are disabled by server policy.\"\n                            else\n                                Ok None\n                        else\n                            let requestedSeconds =\n                                if parameters.ExpiresInSeconds > 0L then\n                                    parameters.ExpiresInSeconds\n                                else\n                                    int64 defaultDays * 86400L\n\n                            if requestedSeconds > maxSeconds then\n                                Error $\"Requested lifetime exceeds max of {maxDays} days.\"\n                            else\n                                let expiresAt = now.Plus(Duration.FromSeconds(float requestedSeconds))\n                                Ok(Some expiresAt)\n\n                    match expiresAtOptionResult with\n                    | Error message ->\n                        let graceError = GraceError.Create message correlationId\n                        return! context |> result400BadRequest graceError\n                    | Ok expiresAtOption ->\n                        let claims = getClaimValues context PrincipalMapper.GraceClaim\n                        let groupIds = getClaimValues context PrincipalMapper.GraceGroupIdClaim\n                        let actor = PersonalAccessToken.CreateActorProxy userId correlationId\n\n                        let! result = actor.CreateToken parameters.TokenName claims groupIds expiresAtOption now correlationId\n\n                        match result with\n                        | Ok created ->\n                            let returnValue = GraceReturnValue.Create created correlationId\n                            return! context |> result200Ok returnValue\n                        | Error error -> return! context |> result400BadRequest error\n            }\n\n    let TokenList (configuration: IConfiguration) : HttpHandler =\n        fun next context ->\n            task {\n                let correlationId = getCorrelationId context\n\n                match PrincipalMapper.tryGetUserId context.User with\n                | None -> return! RequestErrors.UNAUTHORIZED \"Grace\" \"Auth\" \"Authentication required.\" next context\n                | Some userId ->\n                    let! parameters = context.BindJsonAsync<ListPersonalAccessTokensParameters>()\n                    let now = getCurrentInstant ()\n                    let actor = PersonalAccessToken.CreateActorProxy userId correlationId\n                    let! tokens = actor.ListTokens parameters.IncludeRevoked parameters.IncludeExpired now correlationId\n                    let returnValue = GraceReturnValue.Create tokens correlationId\n                    return! context |> result200Ok returnValue\n            }\n\n    let TokenRevoke (configuration: IConfiguration) : HttpHandler =\n        fun next context ->\n            task {\n                let correlationId = getCorrelationId context\n\n                match PrincipalMapper.tryGetUserId context.User with\n                | None -> return! RequestErrors.UNAUTHORIZED \"Grace\" \"Auth\" \"Authentication required.\" next context\n                | Some userId ->\n                    let! parameters = context.BindJsonAsync<RevokePersonalAccessTokenParameters>()\n                    let now = getCurrentInstant ()\n                    let actor = PersonalAccessToken.CreateActorProxy userId correlationId\n                    let! result = actor.RevokeToken parameters.TokenId now correlationId\n\n                    match result with\n                    | Ok summary ->\n                        let returnValue = GraceReturnValue.Create summary correlationId\n                        return! context |> result200Ok returnValue\n                    | Error error -> return! context |> result400BadRequest error\n            }\n"
  },
  {
    "path": "src/Grace.Server/Branch.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Branch\nopen Grace.Types\nopen Grace.Types.Branch\nopen Grace.Types.Diff\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Linq\nopen System.Text.Json\nopen System.Threading.Tasks\n\nmodule Branch =\n    type Validations<'T when 'T :> BranchParameters> = 'T -> ValueTask<Result<unit, BranchError>> array\n\n    let activitySource = new ActivitySource(\"Branch\")\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Branch.Server\")\n\n    let processCommand<'T when 'T :> BranchParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<BranchCommand>) =\n        task {\n            let startTime = getCurrentInstant ()\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                let commandName = context.Items[\"Command\"] :?> string\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                // We know these Id's from ValidateIds.Middleware, so let's set them so we never have to resolve them again.\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                parameters.BranchId <- graceIds.BranchIdString\n\n                let handleCommand cmd =\n                    task {\n                        let actorProxy = Branch.CreateActorProxy graceIds.BranchId graceIds.RepositoryId correlationId\n\n                        //logToConsole\n                        //    $\"In Branch.Server.processCommand: command: {commandName}; OwnerId: {graceIds.OwnerIdString}; OrganizationId: {graceIds.OrganizationIdString}; RepositoryId: {graceIds.RepositoryIdString}; BranchId: {graceIds.BranchIdString}.\"\n\n                        match! actorProxy.Handle cmd (createMetadata context) with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, graceIds.BranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, graceIds.BranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                let validationResults = validations parameters\n\n                let! validationsPassed = validationResults |> allPass\n\n                log.LogDebug(\n                    \"{CurrentInstant}: In Branch.Server.processCommand: validationsPassed: {validationsPassed}.\",\n                    getCurrentInstantExtended (),\n                    validationsPassed\n                )\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let! result = handleCommand cmd\n                    let duration = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {hostName}; Duration: {duration}; CorrelationId: {correlationId}; Finished {path}; Status code: {statusCode}; OwnerId: {ownerId}; OrganizationId: {organizationId}; RepositoryId: {repositoryId}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration,\n                        correlationId,\n                        context.Request.Path,\n                        context.Response.StatusCode,\n                        graceIds.OwnerIdString,\n                        graceIds.OrganizationIdString,\n                        graceIds.RepositoryIdString,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = BranchError.getErrorMessage error\n                    log.LogDebug(\"{CurrentInstant}: error: {error}\", getCurrentInstantExtended (), errorMessage)\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof BranchId, graceIds.BranchId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Branch.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    (getCorrelationId context)\n                )\n\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof BranchId, graceIds.BranchId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> BranchParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        maxCount\n        (query: QueryResult<IBranchActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    // Get the actor proxy for the branch.\n                    let branchGuid = Guid.Parse(graceIds.BranchIdString)\n                    let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n                    let actorProxy = Branch.CreateActorProxy branchGuid repositoryId correlationId\n\n                    // Execute the query.\n                    let! queryResult = query context maxCount actorProxy\n\n                    // Wrap the query result in a GraceReturnValue.\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof BranchId, graceIds.BranchId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError =\n                        (GraceError.Create (BranchError.getErrorMessage error) correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof BranchId, graceIds.BranchId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof BranchId, graceIds.BranchId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Creates a new branch.\n    let Create: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateBranchParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.ParentBranchId BranchError.InvalidBranchId\n                        String.isValidGraceName parameters.ParentBranchName BranchError.InvalidBranchName\n                        Branch.branchExists\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.ParentBranchId\n                            parameters.ParentBranchName\n                            parameters.CorrelationId\n                            BranchError.ParentBranchDoesNotExist\n                        Branch.parentBranchAllowsPromotions\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.ParentBranchId\n                            parameters.ParentBranchName\n                            parameters.CorrelationId\n                            BranchError.ParentBranchDoesNotAllowPromotions\n                        Branch.branchNameDoesNotExist\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchName\n                            parameters.CorrelationId\n                            BranchError.BranchNameAlreadyExists\n                    |]\n\n                let command (parameters: CreateBranchParameters) =\n                    task {\n                        match! (resolveBranchId\n                                    graceIds.OwnerId\n                                    graceIds.OrganizationId\n                                    graceIds.RepositoryId\n                                    parameters.ParentBranchId\n                                    parameters.ParentBranchName\n                                    parameters.CorrelationId)\n                            with\n                        | Some parentBranchId ->\n                            let repositoryId = Guid.Parse(parameters.RepositoryId)\n                            let parentBranchActorProxy = Branch.CreateActorProxy parentBranchId repositoryId parameters.CorrelationId\n\n                            let! parentBranch = parentBranchActorProxy.Get parameters.CorrelationId\n\n                            return\n                                Create(\n                                    graceIds.BranchId,\n                                    (BranchName parameters.BranchName),\n                                    parentBranchId,\n                                    parentBranch.BasedOn.ReferenceId,\n                                    graceIds.OwnerId,\n                                    graceIds.OrganizationId,\n                                    graceIds.RepositoryId,\n                                    parameters.InitialPermissions\n                                )\n                        | None ->\n                            return\n                                Create(\n                                    graceIds.BranchId,\n                                    (BranchName parameters.BranchName),\n                                    Constants.DefaultParentBranchId,\n                                    ReferenceId.Empty, // This is fucked.\n                                    graceIds.OwnerId,\n                                    graceIds.OrganizationId,\n                                    graceIds.RepositoryId,\n                                    parameters.InitialPermissions\n                                )\n                    }\n                    |> ValueTask<BranchCommand>\n\n                context.Items.Add(\"Command\", nameof Create)\n                return! processCommand context validations command\n            }\n\n    /// Rebases a branch on its parent branch.\n    let Rebase: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: RebaseParameters) =\n                    [|\n                        Branch.referenceIdExists parameters.BasedOn graceIds.RepositoryId parameters.CorrelationId BranchError.ReferenceIdDoesNotExist\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.Commit\n                            parameters.CorrelationId\n                            BranchError.CommitIsDisabled\n                    |]\n\n                let command (parameters: RebaseParameters) =\n                    BranchCommand.Rebase parameters.BasedOn\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Rebase)\n                return! processCommand context validations command\n            }\n\n    /// Assigns a specific directory version as the next promotion reference for a branch.\n    let Assign: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                let validations (parameters: AssignParameters) =\n                    [|\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Input.oneOfTheseValuesMustBeProvided\n                            [|\n                                parameters.DirectoryVersionId\n                                parameters.Sha256Hash\n                            |]\n                            BranchError.EitherDirectoryVersionIdOrSha256HashRequired\n                        Branch.branchAllowsAssign\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            parameters.CorrelationId\n                            BranchError.AssignIsDisabled\n                    |]\n\n                let command (parameters: AssignParameters) =\n                    task {\n                        if parameters.DirectoryVersionId <> Guid.Empty then\n                            let directoryVersionActorProxy =\n                                DirectoryVersion.CreateActorProxy parameters.DirectoryVersionId repositoryId parameters.CorrelationId\n\n                            let! directoryVersionDto = directoryVersionActorProxy.Get(parameters.CorrelationId)\n\n                            return\n                                Some(Assign(parameters.DirectoryVersionId, directoryVersionDto.DirectoryVersion.Sha256Hash, ReferenceText parameters.Message))\n                        elif not <| String.IsNullOrEmpty(parameters.Sha256Hash) then\n                            match! getDirectoryVersionBySha256Hash (Guid.Parse(graceIds.RepositoryIdString)) parameters.Sha256Hash parameters.CorrelationId with\n                            | Some directoryVersion ->\n                                return Some(Assign(directoryVersion.DirectoryVersionId, directoryVersion.Sha256Hash, ReferenceText parameters.Message))\n                            | None -> return None\n                        else\n                            return None\n                    }\n\n                let! parameters = context |> parse<AssignParameters>\n                context.Items.Add(\"Command\", nameof Assign)\n                context.Items[ \"AssignParameters\" ] <- parameters\n\n                context.Request.Body.Seek(0L, IO.SeekOrigin.Begin)\n                |> ignore\n\n                match! command parameters with\n                | Some command -> return! processCommand context validations (fun parameters -> ValueTask<BranchCommand>(command))\n                | None ->\n                    return!\n                        context\n                        |> result400BadRequest (\n                            GraceError.Create (getErrorMessage BranchError.EitherDirectoryVersionIdOrSha256HashRequired) (getCorrelationId context)\n                        )\n            }\n\n    /// Creates a promotion reference in the parent of the specified branch, based on the most-recent commit.\n    let Promote: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateReferenceParameters) =\n                    [|\n                        String.isNotEmpty parameters.Message BranchError.MessageIsRequired\n                        String.maxLength parameters.Message 2048 BranchError.StringIsTooLong\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.Promotion\n                            parameters.CorrelationId\n                            BranchError.PromotionIsDisabled\n                    |]\n\n                let command (parameters: CreateReferenceParameters) =\n                    Promote(parameters.DirectoryVersionId, parameters.Sha256Hash, ReferenceText parameters.Message)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Promote)\n                return! processCommand context validations command\n            }\n\n    /// Creates a commit reference pointing to the current root directory version in the branch.\n    let Commit: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateReferenceParameters) =\n                    [|\n                        String.isNotEmpty parameters.Message BranchError.MessageIsRequired\n                        String.maxLength parameters.Message 2048 BranchError.StringIsTooLong\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.Commit\n                            parameters.CorrelationId\n                            BranchError.CommitIsDisabled\n                    |]\n\n                let command (parameters: CreateReferenceParameters) =\n                    BranchCommand.Commit(parameters.DirectoryVersionId, parameters.Sha256Hash, ReferenceText parameters.Message)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Commit)\n                return! processCommand context validations command\n            }\n\n    /// Creates a checkpoint reference pointing to the current root directory version in the branch.\n    let Checkpoint: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateReferenceParameters) =\n                    [|\n                        String.maxLength parameters.Message 2048 BranchError.StringIsTooLong\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.Checkpoint\n                            parameters.CorrelationId\n                            BranchError.CheckpointIsDisabled\n                    |]\n\n                let command (parameters: CreateReferenceParameters) =\n                    BranchCommand.Checkpoint(parameters.DirectoryVersionId, parameters.Sha256Hash, ReferenceText parameters.Message)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Checkpoint)\n                return! processCommand context validations command\n            }\n\n    /// Creates a save reference pointing to the current root directory version in the branch.\n    let Save: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateReferenceParameters) =\n                    [|\n                        String.maxLength parameters.Message 4096 BranchError.StringIsTooLong\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.Save\n                            parameters.CorrelationId\n                            BranchError.SaveIsDisabled\n                    |]\n\n                let command (parameters: CreateReferenceParameters) =\n                    BranchCommand.Save(parameters.DirectoryVersionId, parameters.Sha256Hash, ReferenceText parameters.Message)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Save)\n                return! processCommand context validations command\n            }\n\n    /// Creates a tag reference pointing to the specified root directory version in the branch.\n    let Tag: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateReferenceParameters) =\n                    [|\n                        String.maxLength parameters.Message 2048 BranchError.StringIsTooLong\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.Tag\n                            parameters.CorrelationId\n                            BranchError.TagIsDisabled\n                    |]\n\n                let command (parameters: CreateReferenceParameters) =\n                    BranchCommand.Tag(parameters.DirectoryVersionId, parameters.Sha256Hash, ReferenceText parameters.Message)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Tag)\n                return! processCommand context validations command\n            }\n\n    /// Creates an external reference pointing to the specified root directory version in the branch.\n    let CreateExternal: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateReferenceParameters) =\n                    [|\n                        String.maxLength parameters.Message 2048 BranchError.StringIsTooLong\n                        String.isValidSha256Hash parameters.Sha256Hash BranchError.Sha256HashIsRequired\n                        Branch.branchAllowsReferenceType\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.BranchId\n                            parameters.BranchName\n                            ReferenceType.External\n                            parameters.CorrelationId\n                            BranchError.ExternalIsDisabled\n                    |]\n\n                let command (parameters: CreateReferenceParameters) =\n                    BranchCommand.CreateExternal(parameters.DirectoryVersionId, parameters.Sha256Hash, ReferenceText parameters.Message)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof CreateExternal)\n                return! processCommand context validations command\n            }\n\n\n    /// Enables and disables `grace assign` commands in the provided branch.\n    let EnableAssign: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) =\n                    EnableAssign(parameters.Enabled)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableAssign)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables promotion references in the provided branch.\n    let EnablePromotion: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) =\n                    EnablePromotion(parameters.Enabled)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnablePromotion)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables commit references in the provided branch.\n    let EnableCommit: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) =\n                    EnableCommit(parameters.Enabled)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableCommit)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables checkpoint references in the provided branch.\n    let EnableCheckpoint: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) =\n                    EnableCheckpoint(parameters.Enabled)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableCheckpoint)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables save references in the provided branch.\n    let EnableSave: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) = EnableSave(parameters.Enabled) |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableSave)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables tag references in the provided branch.\n    let EnableTag: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) = EnableTag(parameters.Enabled) |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableTag)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables external references in the provided branch.\n    let EnableExternal: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) =\n                    EnableExternal(parameters.Enabled)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableExternal)\n                return! processCommand context validations command\n            }\n\n    /// Enables and disables auto-rebase for the provided branch.\n    let EnableAutoRebase: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: EnableFeatureParameters) = [||]\n\n                let command (parameters: EnableFeatureParameters) =\n                    EnableAutoRebase(parameters.Enabled)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof EnableAutoRebase)\n                return! processCommand context validations command\n            }\n\n    /// Sets the promotion mode for the provided branch.\n    let SetPromotionMode: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetPromotionModeParameters) = [||]\n\n                let command (parameters: SetPromotionModeParameters) =\n                    let promotionMode =\n                        match parameters.PromotionMode.ToLowerInvariant() with\n                        | \"individualonly\" -> BranchPromotionMode.IndividualOnly\n                        | \"grouponly\" -> BranchPromotionMode.GroupOnly\n                        | \"hybrid\" -> BranchPromotionMode.Hybrid\n                        | _ -> BranchPromotionMode.IndividualOnly\n\n                    SetPromotionMode(promotionMode) |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetPromotionMode)\n                return! processCommand context validations command\n            }\n\n    /// Deletes the provided branch.\n    let Delete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: DeleteBranchParameters) =\n                    [| // If reassigning child branches, validate that at least one parent is provided OR let it default to the deleted branch's parent\n                    // No validation needed here since None is valid (uses deleted branch's parent)\n                    |]\n\n                let command (parameters: DeleteBranchParameters) =\n                    task {\n                        let! newParentBranchIdOption =\n                            task {\n                                if parameters.ReassignChildBranches then\n                                    if not\n                                       <| String.IsNullOrEmpty(parameters.NewParentBranchId)\n                                       || not\n                                          <| String.IsNullOrEmpty(parameters.NewParentBranchName) then\n                                        match!\n                                            resolveBranchId\n                                                graceIds.OwnerId\n                                                graceIds.OrganizationId\n                                                graceIds.RepositoryId\n                                                parameters.NewParentBranchId\n                                                parameters.NewParentBranchName\n                                                parameters.CorrelationId\n                                            with\n                                        | Some branchId -> return Some branchId\n                                        | None -> return None\n                                    else\n                                        return None\n                                else\n                                    return None\n                            }\n\n                        return DeleteLogical(parameters.Force, parameters.DeleteReason, parameters.ReassignChildBranches, newParentBranchIdOption)\n                    }\n                    |> ValueTask<BranchCommand>\n\n                context.Items.Add(\"Command\", nameof DeleteLogical)\n                return! processCommand context validations command\n            }\n\n    /// Updates the parent branch of the provided branch.\n    let UpdateParentBranch: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: UpdateParentBranchParameters) =\n                    [|\n                        Input.eitherIdOrNameMustBeProvided\n                            parameters.NewParentBranchId\n                            parameters.NewParentBranchName\n                            BranchError.EitherBranchIdOrBranchNameRequired\n                        Guid.isValidAndNotEmptyGuid parameters.NewParentBranchId BranchError.InvalidBranchId\n                        String.isValidGraceName parameters.NewParentBranchName BranchError.InvalidBranchName\n                        Branch.branchExists\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.NewParentBranchId\n                            parameters.NewParentBranchName\n                            parameters.CorrelationId\n                            BranchError.ParentBranchDoesNotExist\n                        Branch.parentBranchAllowsPromotions\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            parameters.NewParentBranchId\n                            parameters.NewParentBranchName\n                            parameters.CorrelationId\n                            BranchError.ParentBranchDoesNotAllowPromotions\n                    |]\n\n                let command (parameters: UpdateParentBranchParameters) =\n                    task {\n                        match!\n                            resolveBranchId\n                                graceIds.OwnerId\n                                graceIds.OrganizationId\n                                graceIds.RepositoryId\n                                parameters.NewParentBranchId\n                                parameters.NewParentBranchName\n                                parameters.CorrelationId\n                            with\n                        | Some newParentBranchId -> return BranchCommand.UpdateParentBranch newParentBranchId\n                        | None ->\n                            // This should never happen due to validations, log error for debugging\n                            log.LogError(\n                                \"{CurrentInstant}: Failed to resolve new parent branch ID after validations passed. NewParentBranchId: {NewParentBranchId}, NewParentBranchName: {NewParentBranchName}\",\n                                getCurrentInstantExtended (),\n                                parameters.NewParentBranchId,\n                                parameters.NewParentBranchName\n                            )\n                            // Return Guid.Empty which will be caught by the actor or cause a validation error\n                            return BranchCommand.UpdateParentBranch Guid.Empty\n                    }\n                    |> ValueTask<BranchCommand>\n\n                context.Items.Add(\"Command\", nameof UpdateParentBranch)\n                return! processCommand context validations command\n            }\n\n    /// Gets details about the provided branch.\n    let Get: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetBranchParameters) = [||]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) = actorProxy.Get(getCorrelationId context)\n\n                    let! parameters = context |> parse<GetBranchParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the events handled by this branch.\n    let GetEvents: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetBranchParameters) = [||]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchEvents = actorProxy.GetEvents(getCorrelationId context)\n                            return branchEvents.Select(fun branchEvent -> serialize branchEvent)\n                        //return List<Events.Branch.BranchEvent>()\n                        }\n\n                    let! parameters = context |> parse<GetBranchParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets details about the parent branch of the provided branch.\n    let GetParentBranch: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: BranchParameters) = [||]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! parentBranchDto = actorProxy.GetParentBranch(getCorrelationId context)\n                            return parentBranchDto\n                        }\n\n                    let! parameters = context |> parse<BranchParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets details about the reference with the provided ReferenceId.\n    let GetReference: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                try\n                    let validations (parameters: GetReferenceParameters) = [||]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let referenceGuid = Guid.Parse(context.Items[\"ReferenceId\"] :?> string)\n\n                            let referenceActorProxy = Reference.CreateActorProxy referenceGuid repositoryId (getCorrelationId context)\n\n                            let referenceDto = referenceActorProxy.Get(getCorrelationId context)\n                            return referenceDto\n                        }\n\n                    let! parameters = context |> parse<GetReferenceParameters>\n                    context.Items[ \"ReferenceId\" ] <- parameters.ReferenceId\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets details about multiple references in one API call.\n    let GetReferences: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getReferences branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets a list of references, given a list of reference IDs.\n    let GetLatestReferencesByReferenceTypes: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetLatestReferencesByReferenceTypeParameters) =\n                        [|\n                            Input.listIsNonEmpty parameters.ReferenceTypes BranchError.ReferenceTypeMustBeProvided\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IBranchActor) =\n                        task {\n                            let referenceTypes = context.Items[\"ReferenceTypes\"] :?> ReferenceType array\n\n                            log.LogDebug(\n                                \"In Repository.Server.GetLatestReferencesByReferenceTypes: ReferenceTypes: {referenceTypes}.\",\n                                serialize referenceTypes\n                            )\n\n                            return! getLatestReferenceByReferenceTypes referenceTypes graceIds.RepositoryId graceIds.BranchId\n                        }\n\n                    let! parameters =\n                        context\n                        |> parse<GetLatestReferencesByReferenceTypeParameters>\n\n                    context.Items.Add(\"ReferenceTypes\", serialize parameters.ReferenceTypes)\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Retrieves the diffs between references in a branch by ReferenceType.\n    let GetDiffsForReferenceType: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                try\n                    let validations (parameters: GetDiffsForReferenceTypeParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                            String.isNotEmpty parameters.ReferenceType BranchError.ReferenceTypeMustBeProvided\n                            DiscriminatedUnion.isMemberOf<ReferenceType, BranchError> parameters.ReferenceType BranchError.InvalidReferenceType\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IBranchActor) =\n                        task {\n                            let diffDtos = ConcurrentBag<DiffDto>()\n                            let! branchDto = actorProxy.Get correlationId\n\n                            let referenceType =\n                                (context.Items[nameof ReferenceType] :?> String)\n                                |> discriminatedUnionFromString<ReferenceType>\n\n                            let! references =\n                                getReferencesByType referenceType.Value branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n\n                            let sortedRefs =\n                                references\n                                    .OrderByDescending(fun referenceDto -> referenceDto.CreatedAt)\n                                    .ToList()\n\n                            // Take pairs of references and get the diffs between them.\n                            do!\n                                Parallel.ForEachAsync(\n                                    [ 0 .. sortedRefs.Count - 2 ],\n                                    Constants.ParallelOptions,\n                                    (fun i ct ->\n                                        ValueTask(\n                                            task {\n                                                let diffActorProxy =\n                                                    Diff.CreateActorProxy\n                                                        sortedRefs[i].DirectoryId\n                                                        sortedRefs[i + 1].DirectoryId\n                                                        branchDto.OwnerId\n                                                        branchDto.OrganizationId\n                                                        branchDto.RepositoryId\n                                                        correlationId\n\n                                                let! diffDto = diffActorProxy.GetDiff correlationId\n                                                diffDtos.Add(diffDto)\n                                            }\n                                        ))\n                                )\n\n                            return\n                                (sortedRefs,\n                                 diffDtos\n                                     .OrderByDescending(fun diffDto ->\n                                         Math.Max(diffDto.Directory1CreatedAt.ToUnixTimeMilliseconds(), diffDto.Directory2CreatedAt.ToUnixTimeMilliseconds()))\n                                     .ToList())\n                        }\n\n                    let! parameters =\n                        context\n                        |> parse<GetDiffsForReferenceTypeParameters>\n\n                    context.Items.Add(nameof ReferenceType, parameters.ReferenceType)\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the promotions in a branch.\n    let GetPromotions: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getPromotions branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the commits in a branch.\n    let GetCommits: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getCommits branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the checkpoints in a branch.\n    let GetCheckpoints: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getCheckpoints branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the saves in a branch.\n    let GetSaves: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getSaves branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the tags in a branch.\n    let GetTags: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getTags branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    /// Gets the external references in a branch.\n    let GetExternals: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesParameters) =\n                        [|\n                            Number.isPositiveOrZero parameters.MaxCount BranchError.ValueMustBePositive\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let! branchDto = actorProxy.Get(getCorrelationId context)\n                            let! results = getExternals branchDto.RepositoryId branchDto.BranchId maxCount (getCorrelationId context)\n                            return results\n                        }\n\n                    let! parameters = context |> parse<GetReferencesParameters>\n                    let! result = processQuery context parameters validations (parameters.MaxCount) query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n            }\n\n    let GetRecursiveSize: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let repositoryId = graceIds.RepositoryId\n\n                try\n                    let validations (parameters: ListContentsParameters) =\n                        [|\n                            String.isEmptyOrValidSha256Hash parameters.Sha256Hash BranchError.InvalidSha256Hash\n                            Guid.isValidAndNotEmptyGuid parameters.ReferenceId BranchError.InvalidReferenceId\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let listContentsParameters = context.Items[\"ListContentsParameters\"] :?> ListContentsParameters\n\n                            if\n                                String.IsNullOrEmpty(listContentsParameters.ReferenceId)\n                                && String.IsNullOrEmpty(listContentsParameters.Sha256Hash)\n                            then\n                                // If we don't have a referenceId or sha256Hash, we'll get the contents of the most recent reference in the branch.\n                                let! branchDto = actorProxy.Get correlationId\n                                let! latestReference = getLatestReference branchDto.RepositoryId branchDto.BranchId\n\n                                match latestReference with\n                                | Some latestReference ->\n                                    let directoryActorProxy = DirectoryVersion.CreateActorProxy latestReference.DirectoryId branchDto.RepositoryId correlationId\n\n                                    let! recursiveSize = directoryActorProxy.GetRecursiveSize(getCorrelationId context)\n                                    return recursiveSize\n                                | None -> return Constants.InitialDirectorySize\n                            elif\n                                not\n                                <| String.IsNullOrEmpty(listContentsParameters.ReferenceId)\n                            then\n                                // We have a ReferenceId, so we'll get the DirectoryVersion from that reference.\n                                let referenceGuid = Guid.Parse(listContentsParameters.ReferenceId)\n                                let referenceActorProxy = Reference.CreateActorProxy referenceGuid repositoryId correlationId\n\n                                let! referenceDto = referenceActorProxy.Get correlationId\n\n                                let directoryActorProxy = DirectoryVersion.CreateActorProxy referenceDto.DirectoryId repositoryId correlationId\n\n                                let! recursiveSize = directoryActorProxy.GetRecursiveSize correlationId\n                                return recursiveSize\n                            else\n                                // By process of elimination, we have a Sha256Hash, so we'll retrieve the DirectoryVersion using that..\n                                match! Services.getDirectoryVersionBySha256Hash graceIds.RepositoryId listContentsParameters.Sha256Hash correlationId with\n                                | Some directoryVersion ->\n                                    let directoryActorProxy = DirectoryVersion.CreateActorProxy directoryVersion.DirectoryVersionId repositoryId correlationId\n\n                                    let! recursiveSize = directoryActorProxy.GetRecursiveSize correlationId\n                                    return recursiveSize\n                                | None -> return Constants.InitialDirectorySize\n                        }\n\n                    let! parameters = context |> parse<ListContentsParameters>\n                    context.Items[ \"ListContentsParameters\" ] <- parameters\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty correlationId)\n            }\n\n    let ListContents: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                try\n                    let validations (parameters: ListContentsParameters) =\n                        [|\n                            String.isEmptyOrValidSha256Hash parameters.Sha256Hash BranchError.InvalidSha256Hash\n                            Guid.isValidAndNotEmptyGuid parameters.ReferenceId BranchError.InvalidReferenceId\n                        |]\n\n                    let query (context: HttpContext) maxCount (actorProxy: IBranchActor) =\n                        task {\n                            let listContentsParameters = context.Items[\"ListContentsParameters\"] :?> ListContentsParameters\n\n                            if\n                                String.IsNullOrEmpty(listContentsParameters.ReferenceId)\n                                && String.IsNullOrEmpty(listContentsParameters.Sha256Hash)\n                            then\n                                // If we don't have a referenceId or sha256Hash, we'll get the contents of the most recent reference in the branch.\n                                let! branchDto = actorProxy.Get correlationId\n                                let! latestReference = getLatestReference branchDto.RepositoryId branchDto.BranchId\n\n                                match latestReference with\n                                | Some latestReference ->\n                                    let directoryActorProxy = DirectoryVersion.CreateActorProxy latestReference.DirectoryId graceIds.RepositoryId correlationId\n\n                                    let! contents = directoryActorProxy.GetRecursiveDirectoryVersions listContentsParameters.ForceRecompute correlationId\n\n                                    return contents\n                                | None -> return Array.Empty<DirectoryVersion.DirectoryVersionDto>()\n                            elif\n                                not\n                                <| String.IsNullOrEmpty(listContentsParameters.ReferenceId)\n                            then\n                                // We have a ReferenceId, so we'll get the DirectoryVersion from that reference.\n                                let referenceGuid = Guid.Parse(listContentsParameters.ReferenceId)\n\n                                let referenceActorProxy = Reference.CreateActorProxy referenceGuid repositoryId correlationId\n\n                                let! referenceDto = referenceActorProxy.Get correlationId\n\n                                let directoryActorProxy = DirectoryVersion.CreateActorProxy referenceDto.DirectoryId repositoryId correlationId\n\n                                let! contents = directoryActorProxy.GetRecursiveDirectoryVersions listContentsParameters.ForceRecompute correlationId\n\n                                return contents\n                            else\n                                // By process of elimination, we have a Sha256Hash, so we'll retrieve the DirectoryVersion using that..\n                                match! getRootDirectoryVersionBySha256Hash graceIds.RepositoryId listContentsParameters.Sha256Hash correlationId with\n                                | Some directoryVersion ->\n                                    let directoryActorProxy = DirectoryVersion.CreateActorProxy directoryVersion.DirectoryVersionId repositoryId correlationId\n\n                                    let! contents = directoryActorProxy.GetRecursiveDirectoryVersions listContentsParameters.ForceRecompute correlationId\n\n                                    return contents\n                                | None -> return Array.Empty<DirectoryVersion.DirectoryVersionDto>()\n                        }\n\n                    let! parameters = context |> parse<ListContentsParameters>\n                    context.Items[ \"ListContentsParameters\" ] <- parameters\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty correlationId)\n            }\n\n    let private getVersionValidations (parameters: GetBranchVersionParameters) =\n        [|\n            String.isEmptyOrValidSha256Hash parameters.Sha256Hash BranchError.InvalidSha256Hash\n            Guid.isValidAndNotEmptyGuid parameters.ReferenceId BranchError.InvalidReferenceId\n        |]\n\n    let private tryResolveRootDirectoryVersion\n        (context: HttpContext)\n        (parameters: GetBranchVersionParameters)\n        (actorProxy: IBranchActor)\n        (repositoryId: Guid)\n        (correlationId: CorrelationId)\n        =\n        task {\n            if not <| String.IsNullOrEmpty(parameters.Sha256Hash) then\n                return! getRootDirectoryVersionBySha256Hash repositoryId parameters.Sha256Hash correlationId\n            elif\n                not\n                <| String.IsNullOrEmpty(parameters.ReferenceId)\n            then\n                return! getRootDirectoryVersionByReferenceId repositoryId (Guid.Parse(parameters.ReferenceId)) correlationId\n            else\n                let! branchDto = actorProxy.Get(getCorrelationId context)\n                let! latestReference = getLatestReference branchDto.RepositoryId branchDto.BranchId\n\n                match latestReference with\n                | Some referenceDto -> return! getRootDirectoryVersionBySha256Hash repositoryId referenceDto.Sha256Hash correlationId\n                | None -> return None\n        }\n\n    let private getVersionQuery (context: HttpContext) _maxCount (actorProxy: IBranchActor) =\n        task {\n            let parameters = context.Items[\"GetVersionParameters\"] :?> GetBranchVersionParameters\n            let repositoryId = Guid.Parse(parameters.RepositoryId)\n            let correlationId = getCorrelationId context\n\n            let! rootDirectoryVersion = tryResolveRootDirectoryVersion context parameters actorProxy repositoryId correlationId\n\n            match rootDirectoryVersion with\n            | Some rootDirectoryVersion ->\n                let directoryVersionActorProxy = DirectoryVersion.CreateActorProxy rootDirectoryVersion.DirectoryVersionId repositoryId correlationId\n\n                let! directoryVersionDtos = directoryVersionActorProxy.GetRecursiveDirectoryVersions false correlationId\n\n                let directoryIds =\n                    directoryVersionDtos\n                        .Select(fun dv -> dv.DirectoryVersion.DirectoryVersionId)\n                        .ToList()\n\n                return directoryIds\n            | None -> return List<DirectoryVersionId>()\n        }\n\n    let private updateParametersFromBranch (parameters: GetBranchVersionParameters) (repositoryIdResolved: Guid) =\n        task {\n            logToConsole $\"In Branch.GetVersion: parameters.BranchId: {parameters.BranchId}; parameters.BranchName: {parameters.BranchName}\"\n\n            let! resolvedBranchId =\n                resolveBranchId\n                    parameters.OwnerId\n                    parameters.OrganizationId\n                    repositoryIdResolved\n                    parameters.BranchId\n                    parameters.BranchName\n                    parameters.CorrelationId\n\n            match resolvedBranchId with\n            | Some branchId -> parameters.BranchId <- $\"{branchId}\"\n            | None -> () // This should never happen because it would get caught in validations.\n        }\n\n    let private updateParametersFromReference\n        (context: HttpContext)\n        (parameters: GetBranchVersionParameters)\n        (repositoryIdResolved: Guid)\n        (correlationId: CorrelationId)\n        =\n        task {\n            logToConsole $\"In Branch.GetVersion: parameters.ReferenceId: {parameters.ReferenceId}\"\n            let referenceGuid = Guid.Parse(parameters.ReferenceId)\n            let referenceActorProxy = Reference.CreateActorProxy referenceGuid repositoryIdResolved correlationId\n\n            let! referenceDto = referenceActorProxy.Get(getCorrelationId context)\n            logToConsole $\"referenceDto.ReferenceId: {referenceDto.ReferenceId}\"\n            parameters.BranchId <- $\"{referenceDto.BranchId}\"\n        }\n\n    let private updateParametersFromSha (parameters: GetBranchVersionParameters) (repositoryIdFromRoute: Guid) (graceIds: GraceIds) =\n        task {\n            logToConsole $\"In Branch.GetVersion: parameters.Sha256Hash: {parameters.Sha256Hash}\"\n\n            let! referenceResult = getReferenceBySha256Hash repositoryIdFromRoute graceIds.BranchId parameters.Sha256Hash\n\n            match referenceResult with\n            | Some referenceDto ->\n                logToConsole $\"referenceDto.ReferenceId: {referenceDto.ReferenceId}\"\n                parameters.BranchId <- $\"{referenceDto.BranchId}\"\n            | None ->\n                // Reference Id was not found in the database.\n                ()\n        }\n\n    let private updateParametersForRepositoryIdOption\n        (context: HttpContext)\n        (graceIds: GraceIds)\n        (repositoryIdFromRoute: Guid)\n        (correlationId: CorrelationId)\n        (parameters: GetBranchVersionParameters)\n        (repositoryIdOption: Guid option)\n        =\n        match repositoryIdOption with\n        | None -> Task.FromResult(())\n        | Some repositoryIdResolved ->\n            let repositoryIdString = $\"{repositoryIdResolved}\"\n            parameters.RepositoryId <- repositoryIdString\n\n            if not <| String.IsNullOrEmpty(parameters.BranchId)\n               || not <| String.IsNullOrEmpty(parameters.BranchName) then\n                updateParametersFromBranch parameters repositoryIdResolved\n            elif\n                not\n                <| String.IsNullOrEmpty(parameters.ReferenceId)\n            then\n                updateParametersFromReference context parameters repositoryIdResolved correlationId\n            elif not <| String.IsNullOrEmpty(parameters.Sha256Hash) then\n                updateParametersFromSha parameters repositoryIdFromRoute graceIds\n            else\n                Task.FromResult(())\n\n    let private getVersionImpl (next: HttpFunc) (context: HttpContext) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let repositoryIdFromRoute = graceIds.RepositoryId\n\n            let! parameters = context |> parse<GetBranchVersionParameters>\n\n            let! repositoryIdOption =\n                resolveRepositoryId graceIds.OwnerId graceIds.OrganizationId parameters.RepositoryId parameters.RepositoryName parameters.CorrelationId\n\n            do! updateParametersForRepositoryIdOption context graceIds repositoryIdFromRoute correlationId parameters repositoryIdOption\n\n            // Now that we've populated BranchId for sure...\n            context.Items.Add(\"GetVersionParameters\", parameters)\n            return! processQuery context parameters getVersionValidations 1 getVersionQuery\n        }\n\n    let GetVersion: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                try\n                    return! getVersionImpl next context\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; BranchId: {branchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        context.Request.Path,\n                        graceIds.BranchIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.CreateWithException ex String.Empty correlationId)\n            }\n"
  },
  {
    "path": "src/Grace.Server/CorrelationId.Server.fs",
    "content": "// Create asp.net core middleware to ensure \"X-Correlation-ID\" header is set on all requests and responses.\n"
  },
  {
    "path": "src/Grace.Server/DerivedComputation.Server.fs",
    "content": "namespace Grace.Server\n\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Events\nopen Grace.Types.Policy\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Threading.Tasks\n\nmodule DerivedComputation =\n\n    let log = loggerFactory.CreateLogger(\"DerivedComputation.Server\")\n\n\n    let internal shouldRecordQuickScan referenceType =\n        match referenceType with\n        | ReferenceType.Commit\n        | ReferenceType.Checkpoint\n        | ReferenceType.Promotion -> true\n        | _ -> false\n\n    let handleReferenceEvent (referenceEvent: ReferenceEvent) =\n        task {\n            match referenceEvent.Event with\n            | ReferenceEventType.Created (referenceId,\n                                          ownerId,\n                                          organizationId,\n                                          repositoryId,\n                                          branchId,\n                                          directoryId,\n                                          sha256Hash,\n                                          referenceType,\n                                          referenceText,\n                                          links) ->\n                match referenceType with\n                | _ when shouldRecordQuickScan referenceType ->\n                    let correlationId = referenceEvent.Metadata.CorrelationId\n                    let policyActorProxy = Policy.CreateActorProxy branchId repositoryId correlationId\n\n                    let! policySnapshot =\n                        task {\n                            match! policyActorProxy.GetCurrent correlationId with\n                            | Some snapshot -> return snapshot.PolicySnapshotId\n                            | None -> return PolicySnapshotId String.Empty\n                        }\n\n                    let now = getCurrentInstant ()\n\n                    let validationResult =\n                        { ValidationResultDto.Default with\n                            ValidationResultId = Guid.NewGuid()\n                            OwnerId = ownerId\n                            OrganizationId = organizationId\n                            RepositoryId = repositoryId\n                            ValidationName = \"quick-scan\"\n                            ValidationVersion = \"1.0\"\n                            Output =\n                                {\n                                    Status = ValidationStatus.Pass\n                                    Summary =\n                                        $\"quick-scan recorded for {getDiscriminatedUnionCaseName referenceType}; referenceId={referenceId}; policySnapshotId={policySnapshot}.\"\n                                    ArtifactIds = []\n                                }\n                            OnBehalfOf = [ UserId Constants.GraceSystemUser ]\n                            CreatedAt = now\n                        }\n\n                    let validationResultActorProxy = ValidationResult.CreateActorProxy validationResult.ValidationResultId repositoryId correlationId\n\n                    let metadata = EventMetadata.New correlationId Constants.GraceSystemUser\n\n                    match! validationResultActorProxy.Handle (ValidationResultCommand.Record validationResult) metadata with\n                    | Ok _ ->\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; quick-scan validation recorded for {referenceType} ReferenceId: {referenceId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            correlationId,\n                            getDiscriminatedUnionCaseName referenceType,\n                            referenceId\n                        )\n                    | Error graceError ->\n                        log.LogError(\n                            \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Failed to record quick-scan validation for ReferenceId {referenceId}: {error}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            correlationId,\n                            referenceId,\n                            graceError\n                        )\n                | _ -> ()\n            | _ -> ()\n        }\n\n    let handlePolicyEvent (policyEvent: PolicyEvent) =\n        task {\n            match policyEvent.Event with\n            | SnapshotCreated snapshot ->\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Policy snapshot created: {policySnapshotId}.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    policyEvent.Metadata.CorrelationId,\n                    snapshot.PolicySnapshotId\n                )\n            | Acknowledged (policySnapshotId, _, _) ->\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; Policy snapshot acknowledged: {policySnapshotId}.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    policyEvent.Metadata.CorrelationId,\n                    policySnapshotId\n                )\n        }\n"
  },
  {
    "path": "src/Grace.Server/Diff.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Shared.Parameters.Diff\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Repository\nopen Grace.Shared.Validation.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.AspNetCore.Mvc\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen OpenTelemetry.Trace\nopen System.Globalization\nopen System.Diagnostics\nopen System.Threading.Tasks\nopen System.Text.Json\n\nmodule Diff =\n    type Validations<'T when 'T :> DiffParameters> = 'T -> ValueTask<Result<unit, DiffError>> array\n\n    let activitySource = new ActivitySource(\"Diff\")\n\n    ///let processCommand<'T when 'T :> DiffParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> Task<DiffCommand>) =\n    //    task {\n    //        try\n    //            use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n    //            let! parameters = context |> parse<'T>\n    //            let validationResults = validations parameters context\n    //            let! validationsPassed = validationResults |> areValid\n    //            if validationsPassed then\n    //                let! repositoryId = resolveRepositoryId parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName parameters.RepositoryId parameters.RepositoryName\n    //                match repositoryId with\n    //                | Some repositoryId ->\n    //                    if String.IsNullOrEmpty(parameters.RepositoryId) then parameters.RepositoryId <- repositoryId\n    //                    let actorProxy = getActorProxy repositoryId context\n    //                    let! cmd = command parameters\n    //                    let! result = actorProxy.Handle cmd (Services.createMetadata context)\n    //                    match result with\n    //                        | Ok graceReturn ->\n    //                            match cmd with\n    //                            | Create _ ->\n    //                                let branchId = (BranchId (Guid.NewGuid()))\n    //                                let branchActorId = Branch.GetActorId branchId\n    //                                let branchActor = context.GetService<IActorProxyFactory>().CreateActorProxy<IBranchActor>(branchActorId, ActorName.Branch)\n    //                                let! result = branchActor.Handle (Branch.BranchCommand.Create (branchId, (BranchName Constants.InitialBranchName), (BranchId.Root), (Guid.Parse(parameters.RepositoryId)))) (Services.createMetadata context)\n    //                                match result with\n    //                                | Ok branchGraceReturn ->\n    //                                    do graceReturn.Properties.Add(nameof(BranchId), $\"{branchId}\")\n    //                                    do graceReturn.Properties.Add(nameof(BranchName), Constants.InitialBranchName)\n    //                                    return! context |> result200Ok graceReturn\n    //                                | Error graceError -> return! context |> result400BadRequest graceError\n    //                            | _ ->\n    //                                return! context |> result200Ok graceReturn\n    //                        | Error graceError -> return! context |> result400BadRequest graceError\n    //                | None -> return! context |> result400BadRequest (GraceError.Create (RepositoryError.getErrorMessage RepositoryError.RepositoryDoesNotExist) (getCorrelationId context))\n    //            else\n    //                let! error = validationResults |> getFirstError\n    //                let graceError = GraceError.Create (RepositoryError.getErrorMessage error) (getCorrelationId context)\n    //                graceError.Properties.Add(\"Path\", context.Request.Path.Value)\n    //                return! context |> result400BadRequest graceError\n    //        with ex ->\n    //            return! context |> result500ServerError (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n    //    }\n\n    let processQuery<'T, 'U when 'T :> DiffParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (query: QueryResult<IDiffActor, 'U>)\n        =\n        task {\n            let correlationId = getCorrelationId context\n            let graceIds = getGraceIds context\n\n            try\n                use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n                //let! parameters = context |> parse<'T>\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let actorProxy =\n                        Diff.CreateActorProxy\n                            parameters.DirectoryVersionId1\n                            parameters.DirectoryVersionId2\n                            graceIds.OwnerId\n                            graceIds.OrganizationId\n                            graceIds.RepositoryId\n                            correlationId\n\n                    //// Need to figure this whole part out next.\n                    //// Then add SDK implementation of GetDiff.\n                    //// Then add CLI command to get diff.\n                    //// Then test diffs end-to-end.\n                    //// Then format the CLI output properly.\n                    //| Some diff ->\n                    //| None ->\n\n                    let! queryResult = query context 1 actorProxy\n                    let returnValue = GraceReturnValue.Create queryResult (getCorrelationId context)\n                    returnValue.Properties.Add($\"DirectoryVersionId1\", $\"{parameters.DirectoryVersionId1}\")\n                    returnValue.Properties.Add($\"DirectoryVersionId2\", $\"{parameters.DirectoryVersionId2}\")\n                    return! context |> result200Ok returnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError = GraceError.Create (getErrorOptionMessage error) (getCorrelationId context)\n\n                    graceError.Properties.Add(\"Path\", context.Request.Path.Value)\n                    graceError.Properties.Add($\"DirectoryVersionId1\", $\"{parameters.DirectoryVersionId1}\")\n                    graceError.Properties.Add($\"DirectoryVersionId2\", $\"{parameters.DirectoryVersionId2}\")\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                return!\n                    context\n                    |> result500ServerError (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n        }\n\n    /// Populates the diff actor, without returning the diff. This is meant to be used when generating the diff through reacting to an event.\n    let Populate: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                try\n                    let graceIds = getGraceIds context\n                    let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                    let validations (parameters: PopulateParameters) =\n                        [|\n                            Guid.isNotEmpty parameters.DirectoryVersionId1 DiffError.InvalidDirectoryVersionId\n                            Guid.isNotEmpty parameters.DirectoryVersionId2 DiffError.InvalidDirectoryVersionId\n                            DirectoryVersion.directoryIdExists\n                                parameters.DirectoryVersionId1\n                                repositoryId\n                                parameters.CorrelationId\n                                DiffError.DirectoryDoesNotExist\n                            DirectoryVersion.directoryIdExists\n                                parameters.DirectoryVersionId2\n                                repositoryId\n                                parameters.CorrelationId\n                                DiffError.DirectoryDoesNotExist\n                        |]\n\n                    let query (context: HttpContext) _ (actorProxy: IDiffActor) =\n                        task {\n                            let! populated = actorProxy.Compute(getCorrelationId context)\n                            return populated\n                        }\n\n                    let! parameters = context |> parse<PopulateParameters>\n                    return! processQuery context parameters validations query\n                with\n                | ex ->\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Retrieves the contents of the diff.\n    let GetDiff: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                try\n                    let graceIds = getGraceIds context\n                    let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                    let validations (parameters: GetDiffParameters) =\n                        [|\n                            Guid.isNotEmpty parameters.DirectoryVersionId1 DiffError.InvalidDirectoryVersionId\n                            Guid.isNotEmpty parameters.DirectoryVersionId2 DiffError.InvalidDirectoryVersionId\n                            DirectoryVersion.directoryIdExists\n                                parameters.DirectoryVersionId1\n                                repositoryId\n                                parameters.CorrelationId\n                                DiffError.DirectoryDoesNotExist\n                            DirectoryVersion.directoryIdExists\n                                parameters.DirectoryVersionId2\n                                repositoryId\n                                parameters.CorrelationId\n                                DiffError.DirectoryDoesNotExist\n                        |]\n\n                    let query (context: HttpContext) _ (actorProxy: IDiffActor) =\n                        task {\n                            logToConsole $\"About to call DiffActor.GetDiff().\"\n                            let! diff = actorProxy.GetDiff(getCorrelationId context)\n                            logToConsole $\"After calling DiffActor.GetDiff().\"\n                            return diff\n                        }\n\n                    let! parameters = context |> parse<GetDiffParameters>\n\n                    return! processQuery context parameters validations query\n                with\n                | ex ->\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Retrieves a diff taken by comparing two DirectoryVersions by Sha256Hash.\n    let GetDiffBySha256Hash: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                try\n                    let validations (parameters: GetDiffBySha256HashParameters) =\n                        [|\n                            String.isNotEmpty parameters.Sha256Hash1 DiffError.Sha256HashIsRequired\n                            String.isNotEmpty parameters.Sha256Hash2 DiffError.Sha256HashIsRequired\n                            String.isValidSha256Hash parameters.Sha256Hash1 DiffError.InvalidSha256Hash\n                            String.isValidSha256Hash parameters.Sha256Hash2 DiffError.InvalidSha256Hash\n                        |]\n\n                    let query (context: HttpContext) _ (actorProxy: IDiffActor) =\n                        task {\n                            let! diff = actorProxy.GetDiff(getCorrelationId context)\n                            return diff\n                        }\n\n                    let! parameters = context |> parse<GetDiffBySha256HashParameters>\n\n                    let graceIds = getGraceIds context\n                    let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n                    let! directoryVersionId1 = getDirectoryVersionBySha256Hash repositoryId parameters.Sha256Hash1 (getCorrelationId context)\n                    let! directoryVersionId2 = getDirectoryVersionBySha256Hash repositoryId parameters.Sha256Hash2 (getCorrelationId context)\n\n                    match directoryVersionId1, directoryVersionId2 with\n                    | Some directoryVersionId1, Some directoryVersionId2 ->\n                        parameters.DirectoryVersionId1 <- directoryVersionId1.DirectoryVersionId\n                        parameters.DirectoryVersionId2 <- directoryVersionId2.DirectoryVersionId\n                    | _ -> ()\n\n                    return! processQuery context parameters validations query\n                with\n                | ex ->\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n"
  },
  {
    "path": "src/Grace.Server/DirectoryVersion.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.DirectoryVersion\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Shared.Parameters.DirectoryVersion\nopen Grace.Shared.Resources.Text\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Microsoft.AspNetCore.Http\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\nopen Giraffe.ViewEngine.HtmlElements\nopen System.IO\n\nmodule DirectoryVersion =\n\n    type Validations<'T when 'T :> DirectoryVersionParameters> = 'T -> ValueTask<Result<unit, DirectoryVersionError>> array\n    //type QueryResult<'T, 'U when 'T :> DirectoryParameters> = 'T -> int -> IDirectoryVersionActor ->Task<'U>\n\n    let activitySource = new ActivitySource(\"Branch\")\n\n    let processCommand<'T when 'T :> DirectoryVersionParameters>\n        (context: HttpContext)\n        (validations: Validations<'T>)\n        (command: 'T -> HttpContext -> Task<GraceResult<string>>)\n        =\n        task {\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! cmd = command parameters context\n\n                    match cmd with\n                    | Ok graceReturnValue -> return! context |> result200Ok graceReturnValue\n                    | Error graceError -> return! context |> result400BadRequest graceError\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError = GraceError.Create (DirectoryVersionError.getErrorMessage error) (getCorrelationId context)\n\n                    graceError.Properties.Add(\"Path\", context.Request.Path.Value)\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError = GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context)\n\n                graceError.Properties.Add(\"Path\", context.Request.Path.Value)\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> DirectoryVersionParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        maxCount\n        (query: QueryResult<IDirectoryVersionActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let correlationId = getCorrelationId context\n            let repositoryId = Guid.Parse(parameters.RepositoryId)\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let directoryVersionGuid = Guid.Parse(parameters.DirectoryVersionId)\n                    let actorProxy = DirectoryVersion.CreateActorProxy directoryVersionGuid repositoryId correlationId\n\n                    let! queryResult = query context maxCount actorProxy\n\n                    let graceReturnValue = GraceReturnValue.Create queryResult correlationId\n\n                    let graceIds = getGraceIds context\n                    graceReturnValue.Properties[ nameof OwnerId ] <- graceIds.OwnerId\n                    graceReturnValue.Properties[ nameof OrganizationId ] <- graceIds.OrganizationId\n                    graceReturnValue.Properties[ nameof RepositoryId ] <- graceIds.RepositoryId\n                    graceReturnValue.Properties[ nameof BranchId ] <- graceIds.BranchId\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError = GraceError.Create (DirectoryVersionError.getErrorMessage error) correlationId\n\n                    graceError.Properties.Add(\"Path\", context.Request.Path.Value)\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                return!\n                    context\n                    |> result500ServerError (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" correlationId)\n        }\n\n    /// Create a new directory version.\n    let Create: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: CreateParameters) =\n                    [|\n                        String.isNotEmpty $\"{parameters.DirectoryVersion.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.DirectoryVersion.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                        String.isNotEmpty $\"{parameters.DirectoryVersion.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.DirectoryVersion.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        String.isNotEmpty $\"{parameters.DirectoryVersion.RelativePath}\" DirectoryVersionError.RelativePathMustNotBeEmpty\n                        String.isNotEmpty $\"{parameters.DirectoryVersion.Sha256Hash}\" DirectoryVersionError.Sha256HashIsRequired\n                        String.isValidSha256Hash $\"{parameters.DirectoryVersion.Sha256Hash}\" DirectoryVersionError.InvalidSha256Hash\n                        Repository.repositoryIdExists\n                            graceIds.OrganizationId\n                            $\"{parameters.DirectoryVersion.RepositoryId}\"\n                            parameters.CorrelationId\n                            DirectoryVersionError.RepositoryDoesNotExist\n                    |]\n\n                let command (parameters: CreateParameters) (context: HttpContext) =\n                    task {\n                        let repositoryActorProxy = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId (getCorrelationId context)\n\n                        let! repositoryDto = repositoryActorProxy.Get(getCorrelationId context)\n\n                        let actorProxy =\n                            DirectoryVersion.CreateActorProxy parameters.DirectoryVersion.DirectoryVersionId graceIds.RepositoryId (getCorrelationId context)\n\n                        return! actorProxy.Handle (DirectoryVersionCommand.Create(parameters.DirectoryVersion, repositoryDto)) (Services.createMetadata context)\n                    }\n\n                return! processCommand context validations command\n            }\n\n    /// Get a directory version.\n    let Get: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                let validations (parameters: GetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                        Repository.repositoryIdExists\n                            graceIds.OrganizationId\n                            $\"{parameters.RepositoryId}\"\n                            parameters.CorrelationId\n                            DirectoryVersionError.RepositoryDoesNotExist\n                        DirectoryVersion.directoryIdExists\n                            (Guid.Parse(parameters.DirectoryVersionId))\n                            repositoryId\n                            parameters.CorrelationId\n                            DirectoryVersionError.DirectoryDoesNotExist\n                    |]\n\n                let query (context: HttpContext) (maxCount: int) (actorProxy: IDirectoryVersionActor) =\n                    task {\n                        let! directoryVersionDto = actorProxy.Get(getCorrelationId context)\n                        return directoryVersionDto\n                    }\n\n                let! parameters = context |> parse<GetParameters>\n                return! processQuery context parameters validations 1 query\n            }\n\n    /// Get a directory version and all of its children.\n    let GetDirectoryVersionsRecursive: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                let validations (parameters: GetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                        Repository.repositoryIdExists\n                            graceIds.OrganizationId\n                            $\"{parameters.RepositoryId}\"\n                            parameters.CorrelationId\n                            DirectoryVersionError.RepositoryDoesNotExist\n                        DirectoryVersion.directoryIdExists\n                            (Guid.Parse(parameters.DirectoryVersionId))\n                            repositoryId\n                            parameters.CorrelationId\n                            DirectoryVersionError.DirectoryDoesNotExist\n                    |]\n\n                let query (context: HttpContext) (maxCount: int) (actorProxy: IDirectoryVersionActor) =\n                    task {\n                        let! directoryVersionDtos = actorProxy.GetRecursiveDirectoryVersions false (getCorrelationId context)\n\n                        return directoryVersionDtos :> IEnumerable<DirectoryVersionDto>\n                    }\n\n                let! parameters = context |> parse<GetParameters>\n                return! processQuery context parameters validations 1 query\n            }\n\n    /// Get a list of directory versions by directory ids.\n    let GetByDirectoryIds: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                let validations (parameters: GetByDirectoryIdsParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        Repository.repositoryIdExists\n                            graceIds.OrganizationId\n                            $\"{parameters.RepositoryId}\"\n                            parameters.CorrelationId\n                            DirectoryVersionError.RepositoryDoesNotExist\n                        DirectoryVersion.directoryIdsExist\n                            parameters.DirectoryIds\n                            repositoryId\n                            parameters.CorrelationId\n                            DirectoryVersionError.DirectoryDoesNotExist\n                    |]\n\n                let query (context: HttpContext) (maxCount: int) (actorProxy: IDirectoryVersionActor) =\n                    task {\n                        let directoryVersionDtos = List<DirectoryVersionDto>()\n\n                        let directoryIds = context.Items[nameof GetByDirectoryIdsParameters] :?> List<DirectoryVersionId>\n\n                        for directoryId in directoryIds do\n                            let actorProxy = DirectoryVersion.CreateActorProxy directoryId repositoryId (getCorrelationId context)\n\n                            let! directoryVersionDto = actorProxy.Get(getCorrelationId context)\n                            directoryVersionDtos.Add(directoryVersionDto)\n\n                        return directoryVersionDtos :> IEnumerable<DirectoryVersionDto>\n                    }\n\n                let! parameters = context |> parse<GetByDirectoryIdsParameters>\n                context.Items[ nameof GetByDirectoryIdsParameters ] <- parameters.DirectoryIds\n                return! processQuery context parameters validations 1 query\n            }\n\n    /// Get a directory version by its SHA256 hash.\n    let GetBySha256Hash: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: GetBySha256HashParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        String.isNotEmpty parameters.Sha256Hash DirectoryVersionError.Sha256HashIsRequired\n                        Repository.repositoryIdExists\n                            graceIds.OrganizationId\n                            $\"{parameters.RepositoryId}\"\n                            parameters.CorrelationId\n                            DirectoryVersionError.RepositoryDoesNotExist\n                    |]\n\n                let query (context: HttpContext) (maxCount: int) (actorProxy: IDirectoryVersionActor) =\n                    task {\n                        let parameters = context.Items[nameof GetBySha256HashParameters] :?> GetBySha256HashParameters\n\n                        match!\n                            getDirectoryVersionBySha256Hash (Guid.Parse(parameters.RepositoryId)) (Sha256Hash parameters.Sha256Hash) (getCorrelationId context)\n                            with\n                        | Some directoryVersion -> return directoryVersion\n                        | None -> return DirectoryVersion.Default\n                    }\n\n                let! parameters = context |> parse<GetBySha256HashParameters>\n                context.Items[ nameof GetBySha256HashParameters ] <- parameters\n                return! processQuery context parameters validations 1 query\n            }\n\n    /// Get the Uri of the zip file for a directory version.\n    let GetZipFile: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                let validations (parameters: GetZipFileParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                        Guid.isValidAndNotEmptyGuid $\"{parameters.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                        Repository.repositoryIdExists\n                            graceIds.OrganizationId\n                            $\"{parameters.RepositoryId}\"\n                            parameters.CorrelationId\n                            DirectoryVersionError.RepositoryDoesNotExist\n                        DirectoryVersion.directoryIdExists\n                            (Guid.Parse(parameters.DirectoryVersionId))\n                            repositoryId\n                            parameters.CorrelationId\n                            DirectoryVersionError.DirectoryDoesNotExist\n                    |]\n\n                let query (context: HttpContext) (maxCount: int) (actorProxy: IDirectoryVersionActor) =\n                    task {\n                        let! zipFile = actorProxy.GetZipFileUri(getCorrelationId context)\n                        logToConsole $\"In DirectoryVersion.GetZipFile: zipFile: {zipFile}.\"\n                        return zipFile\n                    }\n\n                let! parameters = context |> parse<GetZipFileParameters>\n                context.Items[ nameof GetZipFileParameters ] <- parameters\n                return! processQuery context parameters validations 1 query\n            }\n\n    /// Save a list of directory versions.\n    let SaveDirectoryVersions: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n\n                let validations (parameters: SaveDirectoryVersionsParameters) =\n                    let mutable allValidations: ValueTask<Result<unit, DirectoryVersionError>> array = Array.Empty()\n\n                    for directoryVersion in parameters.DirectoryVersions do\n                        let validations =\n                            [|\n                                String.isNotEmpty $\"{directoryVersion.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                                Guid.isValidAndNotEmptyGuid $\"{directoryVersion.DirectoryVersionId}\" DirectoryVersionError.InvalidDirectoryVersionId\n                                String.isNotEmpty $\"{directoryVersion.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                                Guid.isValidAndNotEmptyGuid $\"{directoryVersion.RepositoryId}\" DirectoryVersionError.InvalidRepositoryId\n                                String.isNotEmpty $\"{directoryVersion.Sha256Hash}\" DirectoryVersionError.Sha256HashIsRequired\n                                String.isValidSha256Hash $\"{directoryVersion.Sha256Hash}\" DirectoryVersionError.InvalidSha256Hash\n                                String.isNotEmpty $\"{directoryVersion.RelativePath}\" DirectoryVersionError.RelativePathMustNotBeEmpty\n                                Repository.repositoryIdExists\n                                    graceIds.OrganizationId\n                                    $\"{directoryVersion.RepositoryId}\"\n                                    parameters.CorrelationId\n                                    DirectoryVersionError.RepositoryDoesNotExist\n                            |]\n\n                        allValidations <- Array.append allValidations validations\n\n                    allValidations\n\n                let command (parameters: SaveDirectoryVersionsParameters) (context: HttpContext) =\n                    task {\n                        let correlationId = getCorrelationId context\n                        let results = ConcurrentQueue<GraceResult<string>>()\n\n                        let repositoryActorProxy = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n                        let! repositoryDto = repositoryActorProxy.Get(correlationId)\n\n                        do!\n                            Parallel.ForEachAsync(\n                                parameters.DirectoryVersions,\n                                Constants.ParallelOptions,\n                                (fun directoryVersion ct ->\n                                    ValueTask(\n                                        task {\n                                            try\n                                                // Check if the directory version exists. If it doesn't, create it.\n                                                let directoryVersionActor =\n                                                    DirectoryVersion.CreateActorProxy directoryVersion.DirectoryVersionId repositoryId correlationId\n\n                                                let! exists = directoryVersionActor.Exists parameters.CorrelationId\n                                                //logToConsole $\"In SaveDirectoryVersions: {dv.DirectoryId} exists: {exists}\"\n                                                if not <| exists then\n                                                    let! createResult =\n                                                        directoryVersionActor.Handle\n                                                            (DirectoryVersionCommand.Create(directoryVersion, repositoryDto))\n                                                            (createMetadata context)\n\n                                                    results.Enqueue(createResult)\n                                            with\n                                            | ex ->\n                                                let exceptionResponse = Utilities.ExceptionResponse.Create ex\n\n                                                logToConsole\n                                                    $\"****Error in SaveDirectoryVersions: directoryVersion.Directories.Count: {directoryVersion.Directories.Count}; directoryVersion.Files.Count: {directoryVersion.Files.Count}.\"\n\n                                                logToConsole $\"{exceptionResponse}\"\n                                        }\n                                    ))\n                            )\n\n                        let firstError =\n                            results\n                            |> Seq.tryFind (fun result ->\n                                match result with\n                                | Ok _ -> false\n                                | Error _ -> true)\n\n                        match firstError with\n                        | None -> return Ok(GraceReturnValue.Create \"Uploaded new directory versions.\" correlationId)\n                        | Some error ->\n                            let sb = stringBuilderPool.Get()\n\n                            try\n                                for result in results do\n                                    match result with\n                                    | Ok _ -> ()\n                                    | Error error -> sb.Append($\"{error.Error}; \") |> ignore\n\n                                return Error(GraceError.Create (sb.ToString()) correlationId)\n                            finally\n                                stringBuilderPool.Return(sb)\n                    }\n\n                return! processCommand context validations command\n            }\n"
  },
  {
    "path": "src/Grace.Server/Dockerfile",
    "content": "#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.\n\n# Using the SDK image as the base instead of the aspnet image lets us run a shell for debugging.\nFROM mcr.microsoft.com/dotnet/sdk:10.0-preview AS base\nWORKDIR /app\nEXPOSE 5000\nEXPOSE 5001\n\n# Install required packages for a better terminal experience\nRUN apt-get update && apt-get install -y \\\n    bash \\\n    readline-common \\\n    locales \\\n    && apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Set locale to avoid character encoding issues\nRUN echo \"en_US.UTF-8 UTF-8\" > /etc/locale.gen && \\\n    locale-gen en_US.UTF-8 && \\\n    update-locale LANG=en_US.UTF-8\nENV LANG=en_US.UTF-8\nENV LC_ALL=en_US.UTF-8\n\n# Set bash as the default shell\nSHELL [\"/bin/bash\", \"-c\"]\n\n# Add default bashrc with history and keybinding configurations\nRUN echo 'export PS1=\"\\\\u@\\\\h:\\\\w\\\\$ \"' >> /etc/bash.bashrc && \\\n    echo \"HISTFILE=/root/.bash_history\" >> /etc/bash.bashrc && \\\n    echo \"HISTSIZE=1000\" >> /etc/bash.bashrc && \\\n    echo \"HISTFILESIZE=2000\" >> /etc/bash.bashrc\n\n# Add .inputrc for keybinding corrections (up-arrow and others)\nRUN echo '\"\\e[A\": history-search-backward' >> /root/.inputrc && \\\n    echo '\"\\e[B\": history-search-forward' >> /root/.inputrc && \\\n    echo '\"\\e[C\": forward-char' >> /root/.inputrc && \\\n    echo '\"\\e[D\": backward-char' >> /root/.inputrc\n\n# Optional: Install nano or other text editors for better usability\nRUN apt-get update && apt-get install -y nano && apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Make sure bash is used as the default shell in interactive mode\nCMD [\"/bin/bash\"]\n\n# Set the PATH environment variable to include the dotnet tools directory\nENV PATH=\"$PATH:/root/.dotnet/tools\"\n\n# Set environment variables to configure ASP.NET Core\nENV ASPNETCORE_HTTPS_PORT=5001\nENV ASPNETCORE_HTTP_PORT=5000\nENV DOTNET_GENERATE_ASPNET_CERTIFICATE=true\nENV DOTNET_EnableDiagnostics=1\n\n# Install the dotnet tools globally\nRUN dotnet tool install dotnet-dump -g --verbosity q\nRUN dotnet tool install dotnet-gcdump -g --verbosity q\nRUN dotnet tool install dotnet-trace -g --verbosity q\nRUN dotnet tool install dotnet-counters -g --verbosity q\nRUN dotnet tool install dotnet-monitor -g --verbosity q\n\n#================================================================================================\n\nFROM mcr.microsoft.com/dotnet/sdk:10.0-preview AS build\nWORKDIR /src\n\n# Restoring individual projects to take advantage of layer caching.\nCOPY [\"nuget.config\", \"nuget.config\"]\n\nCOPY [\"Grace.Aspire.ServiceDefaults/Grace.Aspire.ServiceDefaults.csproj\", \"Grace.Aspire.ServiceDefaults/\"]\nRUN dotnet restore \"Grace.Aspire.ServiceDefaults/Grace.Aspire.ServiceDefaults.csproj\"\nCOPY Grace.Aspire.ServiceDefaults/ Grace.Aspire.ServiceDefaults/\nRUN dotnet build \"Grace.Aspire.ServiceDefaults/Grace.Aspire.ServiceDefaults.csproj\" --no-restore -c Debug /p:DebugType=full\n\nCOPY [\"CosmosSerializer/CosmosJsonSerializer.csproj\", \"CosmosSerializer/\"]\nRUN dotnet restore \"CosmosSerializer/CosmosJsonSerializer.csproj\"\nCOPY CosmosSerializer/ CosmosSerializer/\nRUN dotnet build \"CosmosSerializer/CosmosJsonSerializer.csproj\" --no-restore -c Debug /p:DebugType=full\n\nCOPY [\"Grace.Types/Grace.Types.fsproj\", \"Grace.Types/\"]\nCOPY [\"Grace.Shared/Grace.Shared.fsproj\", \"Grace.Shared/\"]\nRUN dotnet restore \"Grace.Types/Grace.Types.fsproj\"\nCOPY Grace.Types/ Grace.Types/\nCOPY Grace.Shared/ Grace.Shared/\nRUN dotnet build \"Grace.Types/Grace.Types.fsproj\" --no-restore -c Debug /p:DebugType=full\n\nRUN dotnet restore \"Grace.Shared/Grace.Shared.fsproj\"\nCOPY Grace.Shared/ Grace.Shared/\nRUN dotnet build \"Grace.Shared/Grace.Shared.fsproj\" --no-restore -c Debug /p:DebugType=full\n\nCOPY [\"Grace.Actors/Grace.Actors.fsproj\", \"Grace.Actors/\"]\nRUN dotnet restore \"Grace.Actors/Grace.Actors.fsproj\"\nCOPY Grace.Actors/ Grace.Actors/\nRUN dotnet build \"Grace.Actors/Grace.Actors.fsproj\" --no-restore -c Debug /p:DebugType=full\n\nCOPY [\"Grace.Orleans.CodeGen/Grace.Orleans.CodeGen.csproj\", \"Grace.Orleans.CodeGen/\"]\nRUN dotnet restore \"Grace.Orleans.CodeGen/Grace.Orleans.CodeGen.csproj\"\nCOPY Grace.Orleans.CodeGen/ Grace.Orleans.CodeGen/\nRUN dotnet build \"Grace.Orleans.CodeGen/Grace.Orleans.CodeGen.csproj\" --no-restore -c Debug /p:DebugType=full\n\nCOPY [\"Grace.Server/Grace.Server.fsproj\", \"Grace.Server/\"]\nRUN dotnet restore \"Grace.Server/Grace.Server.fsproj\"\nCOPY Grace.Server/ Grace.Server/\nRUN dotnet build \"Grace.Server/Grace.Server.fsproj\" --no-restore -c Debug /p:DebugType=full\n\nCOPY Grace.Server/web.config /app/publish/web.config\n\n#================================================================================================\n\nFROM build AS publish\nRUN dotnet publish \"Grace.Server/Grace.Server.fsproj\" -c Debug -o /app/publish --verbosity m --no-build /p:UseAppHost=false /p:PublishProfile=Properties/PublishProfiles/DisableContainerBuild.pubxml\n#RUN ls -alR /app/publish\n\n#================================================================================================\n\nFROM base AS final\nWORKDIR /app\nCOPY --from=publish /app/publish .\n\n# Copy .pdb files to a separate directory\nRUN mkdir -p /app/symbols\nRUN find /app -name \"*.pdb\" -exec cp {} /app/symbols/ \\;\n\n# RUN ls -alR /app\n\nENTRYPOINT [\"dotnet\", \"Grace.Server.dll\"]\n"
  },
  {
    "path": "src/Grace.Server/Eventing.Server.fs",
    "content": "namespace Grace.Server\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Artifact\nopen Grace.Types.Automation\nopen Grace.Types.Events\nopen Grace.Types.Policy\nopen Grace.Types.PromotionSet\nopen Grace.Types.Queue\nopen Grace.Types.Reference\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Grace.Types.WorkItem\nopen System\n\nmodule EventingPublisher =\n    let private tryGetGuidFromMetadata (propertyName: string) (metadata: EventMetadata) =\n        match metadata.Properties.TryGetValue(propertyName) with\n        | true, rawValue ->\n            match Guid.TryParse(rawValue) with\n            | true, parsed -> Some parsed\n            | _ -> Option.None\n        | _ -> Option.None\n\n    let private tryGetRepositoryId (metadata: EventMetadata) =\n        tryGetGuidFromMetadata (nameof RepositoryId) metadata\n\n    let private tryGetOwnerId (metadata: EventMetadata) = tryGetGuidFromMetadata (nameof OwnerId) metadata\n\n    let private tryGetOrganizationId (metadata: EventMetadata) = tryGetGuidFromMetadata (nameof OrganizationId) metadata\n\n    let private tryGetActorId (metadata: EventMetadata) (defaultActorId: string) =\n        match metadata.Properties.TryGetValue(\"ActorId\") with\n        | true, actorId when String.IsNullOrWhiteSpace actorId |> not -> actorId\n        | _ -> defaultActorId\n\n    let private envelope\n        (eventType: AutomationEventType)\n        (metadata: EventMetadata)\n        (ownerId: OwnerId)\n        (organizationId: OrganizationId)\n        (repositoryId: RepositoryId)\n        (actorId: string)\n        (dataJson: string)\n        =\n        AutomationEventEnvelope.Create eventType metadata.Timestamp metadata.CorrelationId ownerId organizationId repositoryId actorId dataJson\n\n    let private tryGetTerminalPromotionSetId (links: ReferenceLinkType seq) =\n        links\n        |> Seq.tryPick (fun link ->\n            match link with\n            | ReferenceLinkType.PromotionSetTerminal promotionSetId -> Some promotionSetId\n            | _ -> Option.None)\n\n    let private mapPromotionSetEventType (eventType: PromotionSetEventType) =\n        match eventType with\n        | PromotionSetEventType.Created _ -> AutomationEventType.PromotionSetCreated\n        | PromotionSetEventType.InputPromotionsUpdated _ -> AutomationEventType.PromotionSetUpdated\n        | PromotionSetEventType.RecomputeStarted _ -> AutomationEventType.PromotionSetRecomputeStarted\n        | PromotionSetEventType.StepsUpdated _ -> AutomationEventType.PromotionSetStepsUpdated\n        | PromotionSetEventType.RecomputeFailed _ -> AutomationEventType.PromotionSetRecomputeFailed\n        | PromotionSetEventType.Blocked _ -> AutomationEventType.PromotionSetBlocked\n        | PromotionSetEventType.ApplyStarted -> AutomationEventType.PromotionSetApplyStarted\n        | PromotionSetEventType.Applied _ -> AutomationEventType.PromotionSetApplied\n        | PromotionSetEventType.ApplyFailed _ -> AutomationEventType.PromotionSetApplyFailed\n        | PromotionSetEventType.LogicalDeleted _ -> AutomationEventType.PromotionSetUpdated\n\n    let tryCreateAgentSessionEnvelope\n        (eventType: AutomationEventType)\n        (metadata: EventMetadata)\n        (operationResult: AgentSessionOperationResult)\n        =\n        let actorId =\n            if String.IsNullOrWhiteSpace operationResult.Session.AgentId then\n                tryGetActorId metadata \"AgentSession\"\n            else\n                operationResult.Session.AgentId\n\n        envelope\n            eventType\n            metadata\n            (tryGetOwnerId metadata\n             |> Option.defaultValue OwnerId.Empty)\n            (tryGetOrganizationId metadata\n             |> Option.defaultValue OrganizationId.Empty)\n            (tryGetRepositoryId metadata\n             |> Option.defaultValue RepositoryId.Empty)\n            actorId\n            (serialize operationResult)\n        |> Some\n\n    let tryCreateEnvelope (graceEvent: GraceEvent) =\n        match graceEvent with\n        | PromotionSetEvent promotionSetEvent ->\n            let eventType = mapPromotionSetEventType promotionSetEvent.Event\n\n            envelope\n                eventType\n                promotionSetEvent.Metadata\n                OwnerId.Empty\n                OrganizationId.Empty\n                (tryGetRepositoryId promotionSetEvent.Metadata\n                 |> Option.defaultValue RepositoryId.Empty)\n                (tryGetActorId promotionSetEvent.Metadata \"PromotionSet\")\n                (serialize promotionSetEvent)\n            |> Some\n        | ValidationSetEvent validationSetEvent ->\n            let eventType =\n                match validationSetEvent.Event with\n                | ValidationSetEventType.Created _ -> AutomationEventType.ValidationSetCreated\n                | ValidationSetEventType.Updated _\n                | ValidationSetEventType.LogicalDeleted _ -> AutomationEventType.ValidationSetUpdated\n\n            envelope\n                eventType\n                validationSetEvent.Metadata\n                OwnerId.Empty\n                OrganizationId.Empty\n                (tryGetRepositoryId validationSetEvent.Metadata\n                 |> Option.defaultValue RepositoryId.Empty)\n                (tryGetActorId validationSetEvent.Metadata \"ValidationSet\")\n                (serialize validationSetEvent)\n            |> Some\n        | ValidationResultEvent validationResultEvent ->\n            envelope\n                AutomationEventType.ValidationResultRecorded\n                validationResultEvent.Metadata\n                OwnerId.Empty\n                OrganizationId.Empty\n                (tryGetRepositoryId validationResultEvent.Metadata\n                 |> Option.defaultValue RepositoryId.Empty)\n                (tryGetActorId validationResultEvent.Metadata \"ValidationResult\")\n                (serialize validationResultEvent)\n            |> Some\n        | ArtifactEvent artifactEvent ->\n            envelope\n                AutomationEventType.ArtifactCreated\n                artifactEvent.Metadata\n                OwnerId.Empty\n                OrganizationId.Empty\n                (tryGetRepositoryId artifactEvent.Metadata\n                 |> Option.defaultValue RepositoryId.Empty)\n                (tryGetActorId artifactEvent.Metadata \"Artifact\")\n                (serialize artifactEvent)\n            |> Some\n        | QueueEvent queueEvent ->\n            let eventType =\n                match queueEvent.Event with\n                | PromotionQueueEventType.PromotionSetEnqueued _ -> Some AutomationEventType.PromotionSetEnqueued\n                | PromotionQueueEventType.PromotionSetDequeued _ -> Some AutomationEventType.PromotionSetDequeued\n                | _ -> Option.None\n\n            eventType\n            |> Option.map (fun mappedType ->\n                envelope\n                    mappedType\n                    queueEvent.Metadata\n                    OwnerId.Empty\n                    OrganizationId.Empty\n                    (tryGetRepositoryId queueEvent.Metadata\n                     |> Option.defaultValue RepositoryId.Empty)\n                    (tryGetActorId queueEvent.Metadata \"PromotionQueue\")\n                    (serialize queueEvent))\n        | ReviewEvent reviewEvent ->\n            let ownerId, organizationId, repositoryId, eventType =\n                match reviewEvent.Event with\n                | ReviewEventType.NotesUpserted notes -> notes.OwnerId, notes.OrganizationId, notes.RepositoryId, AutomationEventType.ReviewNotesUpdated\n                | ReviewEventType.CheckpointAdded _ ->\n                    OwnerId.Empty,\n                    OrganizationId.Empty,\n                    (tryGetRepositoryId reviewEvent.Metadata\n                     |> Option.defaultValue RepositoryId.Empty),\n                    AutomationEventType.ReviewCheckpointRecorded\n                | ReviewEventType.FindingResolved _ ->\n                    OwnerId.Empty,\n                    OrganizationId.Empty,\n                    (tryGetRepositoryId reviewEvent.Metadata\n                     |> Option.defaultValue RepositoryId.Empty),\n                    AutomationEventType.ReviewNotesUpdated\n\n            envelope eventType reviewEvent.Metadata ownerId organizationId repositoryId (tryGetActorId reviewEvent.Metadata \"Review\") (serialize reviewEvent)\n            |> Some\n        | ReferenceEvent referenceEvent ->\n            match referenceEvent.Event with\n            | ReferenceEventType.Created (referenceId, ownerId, organizationId, repositoryId, branchId, _, _, referenceType, _, links) ->\n                if referenceType = ReferenceType.Promotion then\n                    match tryGetTerminalPromotionSetId links with\n                    | Some promotionSetId ->\n                        let payload = {| promotionSetId = promotionSetId; targetBranchId = branchId; terminalPromotionReferenceId = referenceId |}\n\n                        envelope\n                            AutomationEventType.PromotionSetApplied\n                            referenceEvent.Metadata\n                            ownerId\n                            organizationId\n                            repositoryId\n                            (tryGetActorId referenceEvent.Metadata \"Reference\")\n                            (serialize payload)\n                        |> Some\n                    | Option.None -> Option.None\n                else\n                    Option.None\n            | _ -> Option.None\n        | WorkItemEvent workItemEvent ->\n            let eventType =\n                match workItemEvent.Event with\n                | WorkItemEventType.ArtifactLinked _ -> AutomationEventType.AgentSummaryAdded\n                | _ -> AutomationEventType.ReviewNotesUpdated\n\n            envelope\n                eventType\n                workItemEvent.Metadata\n                OwnerId.Empty\n                OrganizationId.Empty\n                (tryGetRepositoryId workItemEvent.Metadata\n                 |> Option.defaultValue RepositoryId.Empty)\n                (tryGetActorId workItemEvent.Metadata \"WorkItem\")\n                (serialize workItemEvent)\n            |> Some\n        | PolicyEvent _\n        | OwnerEvent _\n        | BranchEvent _\n        | DirectoryVersionEvent _\n        | OrganizationEvent _\n        | RepositoryEvent _ -> Option.None\n"
  },
  {
    "path": "src/Grace.Server/Grace.Server.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n    <PropertyGroup>\n        <AssemblyInformationalVersion>0.1.0 (Build Time: $([System.DateTime]::UtcNow.ToString(\"u\")))</AssemblyInformationalVersion>\n    </PropertyGroup>\n\t<PropertyGroup>\n\t\t<TargetFramework>net10.0</TargetFramework>\n        <ServerGarbageCollection>true</ServerGarbageCollection>\n        <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>\n        <GenerateDocumentationFile>false</GenerateDocumentationFile>\n        <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <Platforms>AnyCPU;x64</Platforms>\n\t\t<LangVersion>preview</LangVersion>\n\t\t<Version>0.1</Version>\n\t\t<Description>The server module for Grace Version Control System.</Description>\n\t\t<UserSecretsId>f1167a88-7f15-49c3-8ea1-30c2608081c9</UserSecretsId>\n        <TransformWebConfigEnabled>false</TransformWebConfigEnabled>\n        <ContainerBaseImage>mcr.microsoft.com/dotnet/sdk:10.0</ContainerBaseImage>\n\t\t<ContainerImageTags>0.1;latest</ContainerImageTags>\n\t\t<ContainerRepository>scottarbeit/grace-server</ContainerRepository>\n        <EnableSdkContainerDebugging>true</EnableSdkContainerDebugging>\n        <!--<IsPublishable>false</IsPublishable>-->\n\n        <!--\n        <ContainerRegistry>registry.hub.docker.com</ContainerRegistry>\n        <DockerfileContext>.</DockerfileContext>\n        <DockerfileTag>scottarbeit/grace-server</DockerfileTag>\n        <Dockerfile>./Dockerfile</Dockerfile>\n        <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>\n\t\t<DockerComposeProjectPath>..\\docker-compose.dcproj</DockerComposeProjectPath>\n        -->\n\n        <WarningsAsErrors>FS0025</WarningsAsErrors>\n\t\t<NoWarn>1057,3391</NoWarn>\n\t\t<UseAppHost>false</UseAppHost>\n\t\t<PackageProjectUrl>https://github.com/ScottArbeit/Grace</PackageProjectUrl>\n\t\t<OtherFlags>--test:GraphBasedChecking</OtherFlags>\n\t\t<OtherFlags>--test:ParallelOptimization</OtherFlags>\n\t\t<OtherFlags>--test:ParallelIlxGen</OtherFlags>\n\t</PropertyGroup>\n\t<PropertyGroup Condition=\"'$(Configuration)'=='Debug'\">\n\t\t<PublishReadyToRun>false</PublishReadyToRun>\n\t</PropertyGroup>\n    <PropertyGroup Condition=\"'$(Configuration)'=='Release'\">\n        <PublishReadyToRun>true</PublishReadyToRun>\n    </PropertyGroup>\n    <ItemGroup>\n      <Content Remove=\"web.config\" />\n    </ItemGroup>\n    <ItemGroup>\n\t\t<ContainerPort Include=\"5000\" Type=\"tcp\" />\n\t\t<ContainerPort Include=\"5001\" Type=\"tcp\" />\n\t\t<ContainerPort Include=\"50001\" Type=\"tcp\" />\n\t\t<ContainerPort Include=\"57256\" Type=\"tcp\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<Compile Include=\"ApplicationContext.Server.fs\" />\n\t\t<Compile Include=\"Services.Server.fs\" />\n\t\t<Compile Include=\"Validations.Server.fs\" />\n\t\t<Compile Include=\"Security\\PrincipalMapper.Server.fs\" />\n\t\t<Compile Include=\"Security\\ExternalAuthConfig.Server.fs\" />\n                <Compile Include=\"Security\\ClaimMapping.Server.fs\" />\n                <Compile Include=\"Security\\ClaimsTransformation.Server.fs\" />\n                <Compile Include=\"Security\\TestAuth.Server.fs\" />\n\t\t<Compile Include=\"Security\\PersonalAccessTokenAuth.Server.fs\" />\n\t\t<Compile Include=\"Security\\PermissionEvaluator.Server.fs\" />\n\t\t<Compile Include=\"Security\\AuthorizationMiddleware.Server.fs\" />\n\t\t<Compile Include=\"Security\\EndpointAuthorizationManifest.Server.fs\" />\n        <Compile Include=\"OrleansFilters.Server.fs\" />\n\t\t<Compile Include=\"Middleware\\Fake.Middleware.fs\" />\n\t\t<Compile Include=\"Middleware\\HttpSecurityHeaders.Middleware.fs\" />\n\t\t<Compile Include=\"Middleware\\CorrelationId.Middleware.fs\" />\n\t\t<Compile Include=\"Middleware\\ValidateIds.Middleware.fs\" />\n\t\t<Compile Include=\"Middleware\\LogRequestHeaders.Middleware.fs\" />\n\t\t<Compile Include=\"Middleware\\LogAuthorizationFailure.Middleware.fs\" />\n\t\t<Compile Include=\"Middleware\\Timing.Middleware.fs\" />\n\t\t<Compile Include=\"Owner.Server.fs\" />\n\t\t<Compile Include=\"Organization.Server.fs\" />\n\t\t<Compile Include=\"Repository.Server.fs\" />\n\t\t<Compile Include=\"Branch.Server.fs\" />\n                \n                <Compile Include=\"PromotionSet.Server.fs\" />\n                <Compile Include=\"WorkItem.Server.fs\" />\n                <Compile Include=\"Policy.Server.fs\" />\n                <Compile Include=\"ReviewModels.Server.fs\" />\n                <Compile Include=\"Review.Server.fs\" />\n                <Compile Include=\"Queue.Server.fs\" />\n                <Compile Include=\"ValidationSet.Server.fs\" />\n                <Compile Include=\"ValidationResult.Server.fs\" />\n                <Compile Include=\"Artifact.Server.fs\" />\n                <Compile Include=\"ReviewAnalysis.Server.fs\" />\n                <Compile Include=\"Eventing.Server.fs\" />\n                <Compile Include=\"DirectoryVersion.Server.fs\" />\n\t\t<Compile Include=\"Diff.Server.fs\" />\n\t\t<Compile Include=\"Storage.Server.fs\" />\n\t\t<Compile Include=\"Access.Server.fs\" />\n\t\t<Compile Include=\"Auth.Server.fs\" />\n                <Compile Include=\"Reminder.Server.fs\" />\n                <Compile Include=\"ReminderService.Server.fs\" />\n                <Compile Include=\"DerivedComputation.Server.fs\" />\n                <Compile Include=\"Notification.Server.fs\" />\n\t\t<Compile Include=\"Startup.Server.fs\" />\n\t\t<Compile Include=\"Program.Server.fs\" />\n\t\t<None Include=\"Dockerfile\" />\n\t\t<None Include=\"properties\\publishprofiles\\DisableContainerBuild.pubxml\" />\n\t\t<None Update=\"appsettings.Development.json\">\n\t\t  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<None Include=\"web.config\">\n\t\t  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n\t\t</None>\n\t\t<ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Actors\\Grace.Actors.fsproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Orleans.CodeGen\\Grace.Orleans.CodeGen.csproj\" />\n\t\t<ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<PackageReference Include=\"Asp.Versioning.Mvc\" Version=\"8.1.0\" />\n\t\t<PackageReference Include=\"Asp.Versioning.Mvc.ApiExplorer\" Version=\"8.1.0\" />\n\t\t<PackageReference Include=\"Azure.Messaging.ServiceBus\" Version=\"7.20.1\" />\n\t\t<PackageReference Include=\"Azure.Monitor.OpenTelemetry.Exporter\" Version=\"1.5.0\" />\n\t\t<PackageReference Include=\"Azure.Storage.Blobs\" Version=\"12.26.0\" />\n\t\t<PackageReference Include=\"Azure.Storage.Blobs.Batch\" Version=\"12.23.0\" />\n\t\t<PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n\t\t<PackageReference Include=\"dotenv.net\" Version=\"4.0.0\" />\n        <PackageReference Include=\"FSharp.SystemTextJson\" Version=\"1.4.36\" />\n\t\t<PackageReference Include=\"Giraffe\" Version=\"8.2.0\" />\n\t\t<PackageReference Include=\"MessagePack\" Version=\"3.1.4\" />\n\t\t<PackageReference Include=\"MessagePack.Annotations\" Version=\"3.1.4\" />\n\t\t<PackageReference Include=\"MessagePack.NodaTime\" Version=\"3.5.0\" />\n\t\t<PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.AspNetCore.Authentication.OpenIdConnect\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.AspNetCore.SignalR.StackExchangeRedis\" Version=\"10.0.0\" />\n\t\t<PackageReference Include=\"Microsoft.Azure.Cosmos\" Version=\"3.56.0\" />\n\t\t<PackageReference Include=\"Microsoft.OpenApi\" Version=\"3.0.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Clustering.AzureStorage\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Persistence.AzureStorage\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Persistence.Cosmos\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Persistence.Memory\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Persistence.Redis\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Serialization.FSharp\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Serialization.SystemTextJson\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Server\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"Microsoft.Orleans.Streaming.AzureStorage\" Version=\"9.2.1\" />\n\t\t<PackageReference Include=\"OpenTelemetry\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.Console\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.Prometheus.AspNetCore\" Version=\"1.13.1-beta.1\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Exporter.Zipkin\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Extensions.Hosting\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Instrumentation.AspNetCore\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"OpenTelemetry.Instrumentation.Http\" Version=\"1.14.0\" />\n\t\t<PackageReference Include=\"Orleans.Serialization.NodaTime\" Version=\"0.0.4-beta\" />\n\t\t<PackageReference Include=\"OrleansDashboard\" Version=\"8.2.0\" />\n\t\t<PackageReference Include=\"Swashbuckle.AspNetCore.Swagger\" Version=\"10.0.1\" />\n\t\t<PackageReference Include=\"Swashbuckle.AspNetCore.SwaggerGen\" Version=\"10.0.1\" />\n\t\t<PackageReference Include=\"Swashbuckle.AspNetCore.SwaggerUI\" Version=\"10.0.1\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t\t<InternalsVisibleTo Include=\"Grace.Server.Tests\" />\n\t</ItemGroup>\n\t<ItemGroup>\n\t  <None Include=\"instructions.md\" />\n\t  <None Include=\".env\" />\n\t  <Folder Include=\"wwwroot\\\" />\n\t  <Folder Include=\"Properties\\PublishProfiles\\\" />\n\t</ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.Server/Middleware/CorrelationId.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Grace.Shared\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen System\n\n/// Checks the incoming request for an X-Correlation-Id header. If there's no CorrelationId header, it generates one and adds it to the response headers.\ntype CorrelationIdMiddleware(next: RequestDelegate) =\n\n    member this.Invoke(context: HttpContext) =\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way in...\n#if DEBUG\n        let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n        context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof CorrelationIdMiddleware} --> \"\n        //logToConsole $\"{context.Request.Path}; Middleware Trace In: {middlewareTraceHeader}{nameof CorrelationIdMiddleware} --> \"\n#endif\n\n        let correlationId =\n            if context.Request.Headers.ContainsKey(Constants.CorrelationIdHeaderKey) then\n                context\n                    .Request\n                    .Headers[ Constants.CorrelationIdHeaderKey ]\n                    .ToString()\n            else\n                generateCorrelationId ()\n\n        // Add the CorrelationId to HttpContext so it's easily available.\n        context.Items.Add(Constants.CorrelationId, correlationId)\n\n        // Add the CorrelationId to the response headers.\n        context.Response.Headers.Add(Constants.CorrelationIdHeaderKey, correlationId)\n\n        // -----------------------------------------------------------------------------------------------------\n        // Pass control to next middleware instance...\n        let nextTask = next.Invoke(context)\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way out...\n#if DEBUG\n        let middlewareTraceOutHeader = context.Request.Headers[\"X-MiddlewareTraceOut\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceOut\" ] <- $\"{middlewareTraceOutHeader}{nameof CorrelationIdMiddleware} --> \"\n#endif\n        nextTask\n"
  },
  {
    "path": "src/Grace.Server/Middleware/Fake.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Grace.Server\nopen Grace.Shared\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.ObjectPool\nopen System\nopen System.Text\n\n/// Checks the incoming request for an X-Correlation-Id header. If there's no CorrelationId header, it generates one and adds it to the response headers.\ntype FakeMiddleware(next: RequestDelegate) =\n\n    let pooledObjectPolicy = StringBuilderPooledObjectPolicy()\n    let stringBuilderPool = ObjectPool.Create<StringBuilder>(pooledObjectPolicy)\n    let log = ApplicationContext.loggerFactory.CreateLogger(nameof FakeMiddleware)\n\n    member this.Invoke(context: HttpContext) =\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way in...\n#if DEBUG\n        let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof FakeMiddleware} --> \"\n#endif\n\n        let path = context.Request.Path.ToString()\n        logToConsole $\"****In FakeMiddleware; Path: {path}.\"\n\n        // -----------------------------------------------------------------------------------------------------\n        // Pass control to next middleware instance...\n        let nextTask = next.Invoke(context)\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way out...\n#if DEBUG\n        let middlewareTraceOutHeader = context.Request.Headers[\"X-MiddlewareTraceOut\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceOut\" ] <- $\"{middlewareTraceOutHeader}{nameof FakeMiddleware} --> \"\n#endif\n        nextTask\n"
  },
  {
    "path": "src/Grace.Server/Middleware/HttpSecurityHeaders.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Primitives\nopen System.Collections.Generic\nopen Microsoft.Extensions.DependencyInjection\n\ntype HttpSecurityHeadersMiddleware(next: RequestDelegate) =\n\n    let Access_Control_Allow_Credentials = new KeyValuePair<string, StringValues>(\"Access-Control-Allow-Credentials\", new StringValues(\"true\"))\n\n    let Access_Control_Allow_Methods = new KeyValuePair<string, StringValues>(\"Access-Control-Allow-Methods\", new StringValues(\"GET,POST,PUT\"))\n\n    let Access_Control_Allow_Headers = new KeyValuePair<string, StringValues>(\"Access-Control-Allow-Headers\", new StringValues(\"Origin, X-Correlation-Id\"))\n\n    let X_Content_Type_Options = new KeyValuePair<string, StringValues>(\"X-Content-Type-Options\", new StringValues(\"nosniff\"))\n\n    let X_Frame_Options = new KeyValuePair<string, StringValues>(\"X-Frame-Options\", new StringValues(\"SAMEORIGIN\"))\n\n    let X_Permitted_Cross_Domain_Policies = new KeyValuePair<string, StringValues>(\"X-Permitted-Cross-Domain-Policies\", new StringValues(\"none\"))\n\n    let X_XSS_Protection = new KeyValuePair<string, StringValues>(\"X-XSS-Protection\", new StringValues(\"1; mode=block\"))\n\n    let Referrer_Policy = new KeyValuePair<string, StringValues>(\"Referrer-Policy\", new StringValues(\"strict-origin-when-cross-origin\"))\n\n    let Feature_Policy =\n        new KeyValuePair<string, StringValues>(\n            \"Feature-Policy\",\n            new StringValues(\n                \"accelerometer 'none'; ambient-light-sensor 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'\"\n            )\n        )\n\n    member this.Invoke(context: HttpContext) =\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way in...\n#if DEBUG\n        let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof HttpSecurityHeadersMiddleware} --> \"\n#endif\n\n        let headers = context.Response.Headers\n        headers.Add(X_Content_Type_Options)\n        headers.Add(X_XSS_Protection)\n        headers.Add(X_Frame_Options)\n        headers.Add(X_Permitted_Cross_Domain_Policies)\n        headers.Add(Referrer_Policy)\n        headers.Add(Feature_Policy)\n        headers.Add(Access_Control_Allow_Credentials)\n        headers.Add(Access_Control_Allow_Methods)\n        headers.Add(Access_Control_Allow_Headers)\n\n        // -----------------------------------------------------------------------------------------------------\n        // Pass control to next middleware instance...\n        let nextTask = next.Invoke(context)\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way out...\n\n#if DEBUG\n        let middlewareTraceOutHeader = context.Request.Headers[\"X-MiddlewareTraceOut\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceOut\" ] <- $\"{middlewareTraceOutHeader}{nameof HttpSecurityHeadersMiddleware} --> \"\n#endif\n\n        nextTask\n"
  },
  {
    "path": "src/Grace.Server/Middleware/LogAuthorizationFailure.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Threading.Tasks\n\n/// Logs authorization/authentication failures with correlation context so 401/403 responses are diagnosable from server logs.\ntype LogAuthorizationFailureMiddleware(next: RequestDelegate) =\n\n    let log = loggerFactory.CreateLogger($\"{nameof LogAuthorizationFailureMiddleware}.Server\")\n\n    let tryGetCorrelationId (context: HttpContext) =\n        match context.Items.TryGetValue(Constants.CorrelationId) with\n        | true, value ->\n            match value with\n            | :? string as correlationId when not (String.IsNullOrWhiteSpace correlationId) -> correlationId\n            | _ -> String.Empty\n        | _ -> String.Empty\n\n    member _.Invoke(context: HttpContext) =\n#if DEBUG\n        let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n        context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof LogAuthorizationFailureMiddleware} --> \"\n#endif\n        task {\n            do! next.Invoke(context)\n\n            let statusCode = context.Response.StatusCode\n\n            if statusCode = StatusCodes.Status401Unauthorized\n               || statusCode = StatusCodes.Status403Forbidden then\n                let correlationId = tryGetCorrelationId context\n                let authorizationHeaderPresent = context.Request.Headers.ContainsKey(\"Authorization\")\n\n                let userAuthenticated =\n                    not (isNull context.User)\n                    && not (isNull context.User.Identity)\n                    && context.User.Identity.IsAuthenticated\n\n                let endpointDisplayName =\n                    let endpoint = context.GetEndpoint()\n\n                    if isNull endpoint\n                       || String.IsNullOrWhiteSpace endpoint.DisplayName then\n                        \"<unknown>\"\n                    else\n                        endpoint.DisplayName\n\n                let authChallenge = context.Response.Headers.WWWAuthenticate.ToString()\n\n                if log.IsEnabled(LogLevel.Warning) then\n                    log.LogWarning(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Auth response {StatusCode} for {Method} {Path}. Endpoint: {Endpoint}. Authorization header present: {AuthorizationHeaderPresent}. User authenticated: {UserAuthenticated}. Challenge: {Challenge}.\",\n                        getCurrentInstantExtended (),\n                        Environment.MachineName,\n                        correlationId,\n                        statusCode,\n                        context.Request.Method,\n                        context.Request.Path.ToString(),\n                        endpointDisplayName,\n                        authorizationHeaderPresent,\n                        userAuthenticated,\n                        authChallenge\n                    )\n        }\n        :> Task\n\n"
  },
  {
    "path": "src/Grace.Server/Middleware/LogRequestHeaders.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Grace.Server\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.ObjectPool\nopen System\nopen System.Text\n\n/// Checks the incoming request for an X-Correlation-Id header. If there's no CorrelationId header, it generates one and adds it to the response headers.\ntype LogRequestHeadersMiddleware(next: RequestDelegate) =\n\n    let log = loggerFactory.CreateLogger($\"{nameof LogRequestHeadersMiddleware}.Server\")\n\n    member this.Invoke(context: HttpContext) =\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way in...\n#if DEBUG\n        let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof LogRequestHeadersMiddleware} --> \"\n#endif\n        //let path = context.Request.Path.ToString()\n\n        //if path = \"/healthz\" then\n        //    logToConsole $\"In LogRequestHeadersMiddleware.Middleware.fs: Path: {path}.\"\n\n        if log.IsEnabled(LogLevel.Debug) then\n            let sb = stringBuilderPool.Get()\n\n            try\n                let isSensitiveHeader (name: string) =\n                    name.Equals(\"Authorization\", StringComparison.OrdinalIgnoreCase)\n                    || name.Equals(\"Cookie\", StringComparison.OrdinalIgnoreCase)\n                    || name.Contains(\"token\", StringComparison.OrdinalIgnoreCase)\n\n                context.Request.Headers\n                |> Seq.iter (fun kv ->\n                    let value = if isSensitiveHeader kv.Key then \"[REDACTED]\" else kv.Value.ToString()\n                    sb.AppendLine($\"{kv.Key} = {value}\") |> ignore)\n\n                log.LogDebug(\"Request headers: {headers}\", sb.ToString())\n            finally\n                stringBuilderPool.Return(sb)\n\n        // -----------------------------------------------------------------------------------------------------\n        // Pass control to next middleware instance...\n        let nextTask = next.Invoke(context)\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way out...\n#if DEBUG\n        let middlewareTraceOutHeader = context.Request.Headers[\"X-MiddlewareTraceOut\"]\n\n        context.Request.Headers[ \"X-MiddlewareTraceOut\" ] <- $\"{middlewareTraceOutHeader}{nameof LogRequestHeadersMiddleware} --> \"\n#endif\n        nextTask\n"
  },
  {
    "path": "src/Grace.Server/Middleware/Timing.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Grace.Actors.Services\nopen Grace.Actors.Timing\nopen Grace.Actors.Types\nopen Grace.Server\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen System\nopen System.Collections.Generic\n\n/// Records how long the request took to process.\n///\n/// This middleware should be the second in order, after CorrelationIdMiddleware. This will ensure that the CorrelationId is set before we start recording timings.\ntype TimingMiddleware(next: RequestDelegate) =\n\n    member this.Invoke(context: HttpContext) =\n        task {\n            let isInteresting path =\n                match path with\n                | \"/metrics\"\n                | \"/healthz\" -> false\n                | path when path.StartsWith \"/actors\" -> false\n                | _ -> true\n\n            // -----------------------------------------------------------------------------------------------------\n            // On the way in...\n#if DEBUG\n            let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n\n            context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof TimingMiddleware} --> \"\n#endif\n\n            // We expect the CorrelationId to be set at this point.\n            let correlationId = Services.getCorrelationId context\n\n            if isInteresting context.Request.Path.Value then\n                // Mark this as the start of timing this request.\n                addTiming TimingFlag.Initial String.Empty correlationId\n\n            // -----------------------------------------------------------------------------------------------------\n            // Pass control to next middleware instance...\n            do! next.Invoke(context)\n\n            if isInteresting context.Request.Path.Value then\n                // Mark this as the end of timing this request.\n                addTiming TimingFlag.Final String.Empty correlationId\n\n                // Write the timings to the timing log.\n                reportTimings context.Request.Path correlationId\n\n                // Remove this set of timings from the dictionary.\n                removeTiming correlationId\n\n        // -----------------------------------------------------------------------------------------------------\n        // On the way out...\n#if DEBUG\n            let middlewareTraceOutHeader = context.Request.Headers[\"X-MiddlewareTraceOut\"]\n            context.Request.Headers[ \"X-MiddlewareTraceOut\" ] <- $\"{middlewareTraceOutHeader}{nameof TimingMiddleware} --> \"\n#endif\n        }\n"
  },
  {
    "path": "src/Grace.Server/Middleware/ValidateIds.Middleware.fs",
    "content": "namespace Grace.Server.Middleware\n\nopen Grace.Actors.Services\nopen Grace.Server\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Linq\nopen System.Net\nopen System.Reflection\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\nopen Grace.Actors.Constants.ActorName\n\n/// Holds the PropertyInfo for each Entity Id and Name property.\ntype EntityProperties =\n    {\n        OwnerId: PropertyInfo option\n        OwnerName: PropertyInfo option\n        OrganizationId: PropertyInfo option\n        OrganizationName: PropertyInfo option\n        RepositoryId: PropertyInfo option\n        RepositoryName: PropertyInfo option\n        BranchId: PropertyInfo option\n        BranchName: PropertyInfo option\n    }\n\n    static member Default =\n        {\n            OwnerId = None\n            OwnerName = None\n            OrganizationId = None\n            OrganizationName = None\n            RepositoryId = None\n            RepositoryName = None\n            BranchId = None\n            BranchName = None\n        }\n\n/// Examines the body of the incoming request to validate the Ids and Names in the request, and ensure that we know the right Ids. Having the Ids already figured out saves work for the rest of the pipeline.\n///\n/// If the Ids are invalid, it returns 400 Bad Request.\n///\n/// If the Ids and/or Names aren't found, it returns 404 Not Found.\ntype ValidateIdsMiddleware(next: RequestDelegate) =\n\n    let log = ApplicationContext.loggerFactory.CreateLogger($\"{nameof ValidateIdsMiddleware}.Server\")\n\n    /// Holds the request body type for each endpoint.\n    let typeLookup = ConcurrentDictionary<String, Type>()\n\n    /// Holds the property info for each request body type.\n    let propertyLookup = ConcurrentDictionary<Type, EntityProperties>()\n\n    /// Paths that we want to ignore, because they won't have Ids and Names in the body.\n    let ignorePaths = [ \"/healthz\"; \"/notifications\" ]\n\n    /// Gets the parameter type for the endpoint from the endpoint metadata created in Startup.Server.fs.\n    let getBodyType (context: HttpContext) =\n        let path = context.Request.Path.ToString()\n\n        if not\n           <| (ignorePaths\n               |> Seq.exists (fun ignorePath -> path.StartsWith(ignorePath, StringComparison.InvariantCultureIgnoreCase))) then\n            let endpoint = context.GetEndpoint()\n\n            if isNull (endpoint) then\n                log.LogDebug(\"{CurrentInstant}: Path: {path}; Endpoint: null.\", getCurrentInstantExtended (), path)\n                None\n            elif endpoint.Metadata.Count > 0 then\n                let requestBodyType =\n                    endpoint.Metadata\n                    |> Seq.tryFind (fun metadataItem -> metadataItem.GetType().FullName = \"System.RuntimeType\") // The types that we add in Startup.Server.fs show up here as \"System.RuntimeType\".\n                    |> Option.map (fun metadataItem -> metadataItem :?> Type) // Convert the metadata item to a Type.\n\n                if requestBodyType |> Option.isSome then\n                    log.LogDebug(\n                        \"{CurrentInstant}: Path: {path}; Endpoint: {endpoint.DisplayName}; RequestBodyType: {requestBodyType.Value.Name}.\",\n                        getCurrentInstantExtended (),\n                        path,\n                        endpoint.DisplayName,\n                        requestBodyType.Value.Name\n                    )\n\n                requestBodyType\n            else\n                log.LogDebug(\"{CurrentInstant}: Path: {path}; endpoint.Metadata.Count = 0.\", getCurrentInstantExtended (), path)\n\n                None\n        else\n            None\n\n    member this.Invoke(context: HttpContext) =\n        task {\n\n            // -----------------------------------------------------------------------------------------------------\n            // On the way in...\n            let startTime = getCurrentInstant ()\n\n#if DEBUG\n            let middlewareTraceHeader = context.Request.Headers[\"X-MiddlewareTraceIn\"]\n\n            context.Request.Headers[ \"X-MiddlewareTraceIn\" ] <- $\"{middlewareTraceHeader}{nameof ValidateIdsMiddleware} --> \"\n#endif\n\n            try\n                /// The path of the current request.\n                let path = context.Request.Path.ToString()\n\n                let correlationId = getCorrelationId context\n                let mutable requestBodyType: Type = null\n                let mutable graceIds = GraceIds.Default\n                let mutable (badRequest: GraceError option) = None\n\n                // Get the parameter type for the endpoint from the cache.\n                // If we don't already have it, get it, and add it to the cache.\n                if\n                    not\n                    <| typeLookup.TryGetValue(path, &requestBodyType)\n                then\n                    match getBodyType context with\n                    | Some t ->\n                        requestBodyType <- t\n                        typeLookup.TryAdd(path, requestBodyType) |> ignore\n                    | None -> typeLookup.TryAdd(path, null) |> ignore\n\n                // If we have a request body type for the endpoint, parse the body of the request to get the Ids and Names.\n                // If the request body type is null, it's an endpoint (like /healthz) where we don't take these parameters.\n                if not <| isNull (requestBodyType) then\n                    // This allows us to read the request body multiple times.\n                    context.Request.EnableBuffering()\n\n                    // Deserialize the request body to the type for this endpoint.\n                    match! deserializeToType requestBodyType context with\n                    | None -> ()\n                    | Some requestBody ->\n                        let mutable entityProperties = EntityProperties.Default\n\n                        // Get the available entity properties for this endpoint from the cache.\n                        //   If we don't already have them, figure out which properties are available for this type, and cache that.\n                        if\n                            not\n                            <| propertyLookup.TryGetValue(requestBodyType, &entityProperties)\n                        then\n\n                            // Get all of the properties on the request body type.\n                            let properties = requestBodyType.GetProperties(BindingFlags.Public ||| BindingFlags.Instance)\n\n                            /// Checks if a property with the given name exists on the request body type.\n                            let findProperty name =\n                                properties\n                                |> Seq.tryFind (fun property -> property.Name = name)\n\n                            // Check if these entity properties exist on the request body type.\n                            entityProperties <-\n                                {\n                                    OwnerId = findProperty (nameof OwnerId)\n                                    OwnerName = findProperty (nameof OwnerName)\n                                    OrganizationId = findProperty (nameof OrganizationId)\n                                    OrganizationName = findProperty (nameof OrganizationName)\n                                    RepositoryId = findProperty (nameof RepositoryId)\n                                    RepositoryName = findProperty (nameof RepositoryName)\n                                    BranchId = findProperty (nameof BranchId)\n                                    BranchName = findProperty (nameof BranchName)\n                                }\n\n                            // Cache the property list for this request body type.\n                            propertyLookup.TryAdd(requestBodyType, entityProperties)\n                            |> ignore\n\n                        // Get Owner information.\n                        if entityProperties.OwnerId.IsSome\n                           && entityProperties.OwnerName.IsSome then\n                            // Get the values from the request body.\n                            let ownerIdString = entityProperties.OwnerId.Value.GetValue(requestBody) :?> string\n                            let ownerName = entityProperties.OwnerName.Value.GetValue(requestBody) :?> string\n\n                            let validations =\n                                if path.Equals(\"/owner/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    [|\n                                        Common.String.isNotEmpty ownerIdString OwnerError.OwnerIdIsRequired\n                                        Common.Guid.isValidAndNotEmptyGuid ownerIdString OwnerError.InvalidOwnerId\n                                        Common.String.isNotEmpty ownerName OwnerError.OwnerNameIsRequired\n                                        Common.String.isValidGraceName ownerName OwnerError.InvalidOwnerName\n                                        Common.Input.eitherIdOrNameMustBeProvided ownerIdString ownerName OwnerError.EitherOwnerIdOrOwnerNameRequired\n                                    |]\n                                else\n                                    [|\n                                        Common.Guid.isValidAndNotEmptyGuid ownerIdString OwnerError.InvalidOwnerId\n                                        Common.String.isValidGraceName ownerName OwnerError.InvalidOwnerName\n                                        Common.Input.eitherIdOrNameMustBeProvided ownerIdString ownerName OwnerError.EitherOwnerIdOrOwnerNameRequired\n                                    |]\n\n                            match! getFirstError validations with\n                            | Some error -> badRequest <- Some(GraceError.Create (OwnerError.getErrorMessage error) correlationId)\n                            | None ->\n                                if path.Equals(\"/owner/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    // If we're creating a new Owner, we don't need to resolve the Id.\n                                    graceIds <- { graceIds with OwnerId = Guid.Parse(ownerIdString); OwnerIdString = ownerIdString; HasOwner = true }\n                                else\n                                    // Resolve the OwnerId based on the provided Id and Name.\n                                    match! resolveOwnerId ownerIdString ownerName correlationId with\n                                    | Some resolvedOwnerId ->\n                                        graceIds <- { graceIds with OwnerId = Guid.Parse(resolvedOwnerId); OwnerIdString = resolvedOwnerId; HasOwner = true }\n                                    | None ->\n                                        badRequest <-\n                                            if not <| String.IsNullOrEmpty(ownerIdString) then\n                                                Some(GraceError.Create (getErrorMessage OwnerError.OwnerIdDoesNotExist) correlationId)\n                                            else\n                                                Some(GraceError.Create (getErrorMessage OwnerError.OwnerDoesNotExist) correlationId)\n\n                        // Get Organization information.\n                        if badRequest.IsNone\n                           && entityProperties.OrganizationId.IsSome\n                           && entityProperties.OrganizationName.IsSome then\n                            // Get the values from the request body.\n                            let organizationIdString = entityProperties.OrganizationId.Value.GetValue(requestBody) :?> string\n                            let organizationName = entityProperties.OrganizationName.Value.GetValue(requestBody) :?> string\n\n                            let validations =\n                                if path.Equals(\"/organization/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    [|\n                                        Common.String.isNotEmpty organizationIdString OrganizationError.OrganizationIdIsRequired\n                                        Common.Guid.isValidAndNotEmptyGuid organizationIdString OrganizationError.InvalidOrganizationId\n                                        Common.String.isNotEmpty organizationName OrganizationError.OrganizationNameIsRequired\n                                        Common.String.isValidGraceName organizationName OrganizationError.InvalidOrganizationName\n                                        Common.Input.eitherIdOrNameMustBeProvided\n                                            organizationIdString\n                                            organizationName\n                                            OrganizationError.EitherOrganizationIdOrOrganizationNameRequired\n                                    |]\n                                else\n                                    [|\n                                        Common.Guid.isValidAndNotEmptyGuid organizationIdString OrganizationError.InvalidOrganizationId\n                                        Common.String.isValidGraceName organizationName OrganizationError.InvalidOrganizationName\n                                        Common.Input.eitherIdOrNameMustBeProvided\n                                            organizationIdString\n                                            organizationName\n                                            OrganizationError.EitherOrganizationIdOrOrganizationNameRequired\n                                    |]\n\n                            match! getFirstError validations with\n                            | Some error -> badRequest <- Some(GraceError.Create (OrganizationError.getErrorMessage error) correlationId)\n                            | None ->\n                                if path.Equals(\"/organization/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    // If we're creating a new Organization, we don't need to resolve the Id.\n                                    graceIds <-\n                                        { graceIds with\n                                            OrganizationId = Guid.Parse(organizationIdString)\n                                            OrganizationIdString = organizationIdString\n                                            HasOrganization = true\n                                        }\n                                else\n                                    // Resolve the OrganizationId based on the provided Id and Name.\n                                    match! resolveOrganizationId graceIds.OwnerId organizationIdString organizationName correlationId with\n                                    | Some resolvedOrganizationId ->\n                                        graceIds <-\n                                            { graceIds with\n                                                OrganizationId = Guid.Parse(resolvedOrganizationId)\n                                                OrganizationIdString = resolvedOrganizationId\n                                                HasOrganization = true\n                                            }\n                                    | None ->\n                                        badRequest <-\n                                            if not <| String.IsNullOrEmpty(organizationIdString) then\n                                                Some(GraceError.Create (getErrorMessage OrganizationError.OrganizationIdDoesNotExist) correlationId)\n                                            else\n                                                Some(GraceError.Create (getErrorMessage OrganizationError.OrganizationDoesNotExist) correlationId)\n\n                        // Get repository information.\n                        if badRequest.IsNone\n                           && entityProperties.RepositoryId.IsSome\n                           && entityProperties.RepositoryName.IsSome then\n                            // Get the values from the request body.\n                            let repositoryIdString = entityProperties.RepositoryId.Value.GetValue(requestBody) :?> string\n                            let repositoryName = entityProperties.RepositoryName.Value.GetValue(requestBody) :?> string\n\n                            let validations =\n                                if path.Equals(\"/repository/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    [|\n                                        Common.String.isNotEmpty repositoryIdString RepositoryError.RepositoryIdIsRequired\n                                        Common.Guid.isValidAndNotEmptyGuid repositoryIdString RepositoryError.InvalidRepositoryId\n                                        Common.String.isNotEmpty repositoryName RepositoryError.RepositoryNameIsRequired\n                                        Common.String.isValidGraceName repositoryName RepositoryError.InvalidRepositoryName\n                                        Common.Input.eitherIdOrNameMustBeProvided repositoryIdString repositoryName EitherRepositoryIdOrRepositoryNameRequired\n                                    |]\n                                else\n                                    [|\n                                        Common.Guid.isValidAndNotEmptyGuid repositoryIdString RepositoryError.InvalidRepositoryId\n                                        Common.String.isValidGraceName repositoryName RepositoryError.InvalidRepositoryName\n                                        Common.Input.eitherIdOrNameMustBeProvided repositoryIdString repositoryName EitherRepositoryIdOrRepositoryNameRequired\n                                    |]\n\n                            match! getFirstError validations with\n                            | Some error -> badRequest <- Some(GraceError.Create (RepositoryError.getErrorMessage error) correlationId)\n                            | None ->\n                                if path.Equals(\"/repository/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    // If we're creating a new Repository, we don't need to resolve the Id.\n                                    graceIds <-\n                                        { graceIds with\n                                            RepositoryId = Guid.Parse(repositoryIdString)\n                                            RepositoryIdString = repositoryIdString\n                                            HasRepository = true\n                                        }\n                                else\n                                    // Resolve the RepositoryId based on the provided Id and Name.\n                                    match! resolveRepositoryId graceIds.OwnerId graceIds.OrganizationId repositoryIdString repositoryName correlationId with\n                                    | Some resolvedRepositoryId ->\n                                        graceIds <-\n                                            { graceIds with\n                                                RepositoryId = resolvedRepositoryId\n                                                RepositoryIdString = $\"{resolvedRepositoryId}\"\n                                                HasRepository = true\n                                            }\n                                    | None ->\n                                        badRequest <-\n                                            if not <| String.IsNullOrEmpty(repositoryIdString) then\n                                                Some(GraceError.Create (getErrorMessage RepositoryError.RepositoryIdDoesNotExist) correlationId)\n                                            else\n                                                Some(GraceError.Create (getErrorMessage RepositoryError.RepositoryDoesNotExist) correlationId)\n\n                        // Get branch information.\n                        if badRequest.IsNone\n                           && entityProperties.BranchId.IsSome\n                           && entityProperties.BranchName.IsSome then\n                            // Get the values from the request body.\n                            let branchIdString = entityProperties.BranchId.Value.GetValue(requestBody) :?> string\n                            let branchName = entityProperties.BranchName.Value.GetValue(requestBody) :?> string\n\n                            let validations =\n                                if path.Equals(\"/branch/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    [|\n                                        Common.String.isNotEmpty branchIdString BranchError.BranchIdIsRequired\n                                        Common.Guid.isValidAndNotEmptyGuid branchIdString BranchError.InvalidBranchId\n                                        Common.String.isNotEmpty branchName BranchError.BranchNameIsRequired\n                                        Common.String.isValidGraceName branchName BranchError.InvalidBranchName\n                                        Common.Input.eitherIdOrNameMustBeProvided branchIdString branchName BranchError.EitherBranchIdOrBranchNameRequired\n                                    |]\n                                else\n                                    [|\n                                        Common.Guid.isValidAndNotEmptyGuid branchIdString BranchError.InvalidBranchId\n                                        Common.String.isValidGraceName branchName BranchError.InvalidBranchName\n                                        Common.Input.eitherIdOrNameMustBeProvided branchIdString branchName BranchError.EitherBranchIdOrBranchNameRequired\n                                    |]\n\n                            match! getFirstError validations with\n                            | Some error -> badRequest <- Some(GraceError.Create (BranchError.getErrorMessage error) correlationId)\n                            | None ->\n                                // If we're creating a new Branch, we don't need to resolve the Id.\n                                if path.Equals(\"/branch/create\", StringComparison.InvariantCultureIgnoreCase) then\n                                    let mutable branchId = Guid.Empty\n                                    Guid.TryParse(branchIdString, &branchId) |> ignore\n                                    graceIds <- { graceIds with BranchId = branchId; BranchIdString = branchIdString; HasBranch = true }\n                                else\n                                    // Resolve the BranchId based on the provided Id and Name.\n                                    match!\n                                        resolveBranchId graceIds.OwnerId graceIds.OrganizationId graceIds.RepositoryId branchIdString branchName correlationId\n                                        with\n                                    | Some resolvedBranchId ->\n                                        graceIds <- { graceIds with BranchId = resolvedBranchId; BranchIdString = $\"{resolvedBranchId}\"; HasBranch = true }\n                                    | None ->\n                                        badRequest <-\n                                            if not <| String.IsNullOrEmpty(branchIdString) then\n                                                Some(GraceError.Create (getErrorMessage BranchError.BranchIdDoesNotExist) correlationId)\n                                            else\n                                                Some(GraceError.Create (getErrorMessage BranchError.BranchDoesNotExist) correlationId)\n\n                    // Add the parsed Id's and Names to the HttpContext.\n                    context.Items.Add(nameof GraceIds, graceIds)\n\n                    // Reset Request.Body to position 0 so it can be read again by endpoints.\n                    context.Request.Body.Seek(0L, IO.SeekOrigin.Begin)\n                    |> ignore\n\n                let duration_ms = getDurationRightAligned_ms startTime\n\n                if Option.isSome badRequest then\n                    let error = badRequest.Value\n                    context.Items.Add(\"BadRequest\", error.Error)\n\n                    log.LogWarning(\n                        \"{CurrentInstant}: Node: {hostName}; CorrelationId: {correlationId}; {currentFunction}: Path: {path}; {message}; Duration: {duration_ms}ms.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        nameof ValidateIdsMiddleware,\n                        path,\n                        error.Error,\n                        duration_ms\n                    )\n\n                    let! _ = (context |> result400BadRequest error)\n                    ()\n                else\n                    if graceIds.HasBranch then\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; ValidateIds.Middleware: Path: {path}; OwnerId: {OwnerId}; OrganizationId: {OrganizationId}; RepositoryId: {RepositoryId}; BranchId: {BranchId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            duration_ms,\n                            correlationId,\n                            path,\n                            graceIds.OwnerIdString,\n                            graceIds.OrganizationIdString,\n                            graceIds.RepositoryIdString,\n                            graceIds.BranchIdString\n                        )\n                    elif graceIds.HasRepository then\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; ValidateIds.Middleware: Path: {path}; OwnerId: {OwnerId}; OrganizationId: {OrganizationId}; RepositoryId: {RepositoryId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            duration_ms,\n                            correlationId,\n                            path,\n                            graceIds.OwnerIdString,\n                            graceIds.OrganizationIdString,\n                            graceIds.RepositoryIdString\n                        )\n                    elif graceIds.HasOrganization then\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; ValidateIds.Middleware: Path: {path}; OwnerId: {OwnerId}; OrganizationId: {OrganizationId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            duration_ms,\n                            correlationId,\n                            path,\n                            graceIds.OwnerIdString,\n                            graceIds.OrganizationIdString\n                        )\n                    elif graceIds.HasOwner then\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; ValidateIds.Middleware: Path: {path}; OwnerId: {OwnerId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            duration_ms,\n                            correlationId,\n                            path,\n                            graceIds.OwnerIdString\n                        )\n\n                    // -----------------------------------------------------------------------------------------------------\n\n                    // Pass control to next middleware instance...\n                    //logToConsole\n                    //    $\"********About to call next.Invoke(context) in ValidateIds.Middleware.fs. CorrelationId: {correlationId}. Path: {context.Request.Path}; RepositoryId: {graceIds.RepositoryId}.\"\n\n                    let nextTask = next.Invoke(context)\n\n                    //logToConsole\n                    //    $\"********After call to next.Invoke(context) in ValidateIds.Middleware.fs. CorrelationId: {correlationId}. Path: {context.Request.Path}; RepositoryId: {graceIds.RepositoryId}.\"\n\n                    // -----------------------------------------------------------------------------------------------------\n                    // On the way out...\n\n#if DEBUG\n                    let middlewareTraceOutHeader = context.Request.Headers[\"X-MiddlewareTraceOut\"]\n\n                    context.Request.Headers[ \"X-MiddlewareTraceOut\" ] <- $\"{middlewareTraceOutHeader}{nameof ValidateIdsMiddleware} --> \"\n\n                    if not\n                       <| (ignorePaths\n                           |> Seq.exists (fun ignorePath -> path.StartsWith(ignorePath, StringComparison.InvariantCultureIgnoreCase))) then\n                        log.LogDebug(\n                            \"{CurrentInstant}: Path: {path}; Elapsed: {elapsed}ms; Status code: {statusCode}; graceIds: {graceIds}\",\n                            getCurrentInstantExtended (),\n                            context.Request.Path,\n                            duration_ms,\n                            context.Response.StatusCode,\n                            serialize graceIds\n                        )\n#endif\n                    do! nextTask\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: An unhandled exception occurred in the {middlewareName} middleware.\",\n                    getCurrentInstantExtended (),\n                    nameof ValidateIdsMiddleware\n                )\n\n                context.Response.StatusCode <- 500\n\n                do! context.Response.WriteAsync($\"{getCurrentInstantExtended ()}: An unhandled exception occurred in the ValidateIdsMiddleware middleware.\")\n\n        }\n        :> Task\n"
  },
  {
    "path": "src/Grace.Server/Notification.Server.fs",
    "content": "namespace Grace.Server\n\nopen Azure.Core\nopen Azure.Identity\nopen Azure.Messaging.ServiceBus\nopen FSharp.Control\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.DerivedComputation\nopen Grace.Shared\nopen Grace.Shared.AzureEnvironment\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types\nopen Grace.Types.Automation\nopen Grace.Types.Events\nopen Grace.Types.Queue\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.AspNetCore.Builder\nopen Microsoft.AspNetCore.SignalR\nopen Microsoft.Extensions.Configuration\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Hosting\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Linq\nopen System.Text.Json\nopen System.Text.RegularExpressions\nopen System.Threading\nopen System.Threading.Tasks\n\nmodule Notification =\n\n    let log = loggerFactory.CreateLogger(\"Notification.Server\")\n    let private defaultAzureCredential = lazy (DefaultAzureCredential())\n\n    type IGraceClientConnection =\n        abstract member RegisterRepository: RepositoryId -> Task\n        abstract member RegisterParentBranch: BranchId -> BranchId -> Task\n        abstract member NotifyRepository: RepositoryId * ReferenceId -> Task\n        abstract member NotifyOnCommit: BranchName * BranchName * BranchId * ReferenceId -> Task\n        abstract member NotifyOnCheckpoint: BranchName * BranchName * BranchId * ReferenceId -> Task\n        abstract member NotifyOnSave: BranchName * BranchName * BranchId * ReferenceId -> Task\n        abstract member NotifyAutomationEvent: AutomationEventEnvelope -> Task\n        abstract member ServerToClientMessage: string -> Task\n\n    type NotificationHub() =\n        inherit Hub<IGraceClientConnection>()\n\n        override this.OnConnectedAsync() =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; ConnectionId: {ConnectionId} established.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    this.Context.ConnectionId\n                )\n            }\n\n        member this.RegisterRepository(repositoryId: RepositoryId) =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; ConnectionId: {ConnectionId} registering for RepositoryId: {RepositoryId}.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    this.Context.ConnectionId,\n                    repositoryId\n                )\n\n                do! this.Groups.AddToGroupAsync(this.Context.ConnectionId, $\"{repositoryId}\")\n            }\n\n        member this.RegisterParentBranch(branchId: BranchId, parentBranchId: BranchId) =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; ConnectionId: {ConnectionId} registering for ParentBranchId: {ParentBranchId}.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    this.Context.ConnectionId,\n                    parentBranchId\n                )\n\n                do! this.Groups.AddToGroupAsync(this.Context.ConnectionId, $\"{parentBranchId}\")\n            }\n\n        member this.NotifyRepository((repositoryId: RepositoryId), (referenceId: ReferenceId)) =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; Notifying clients in RepositoryId group: {RepositoryId} of ReferenceId: {ReferenceId}.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    repositoryId,\n                    referenceId\n                )\n\n                do!\n                    this\n                        .Clients\n                        .Group($\"{repositoryId}\")\n                        .NotifyRepository(repositoryId, referenceId)\n            }\n            :> Task\n\n        member this.NotifyOnSave((branchName: BranchName), (parentBranchName: BranchName), (parentBranchId: BranchId), (referenceId: ReferenceId)) =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; Notifying clients with ParentBranch '{ParentBranchName}' ({ParentBranchId}) of save ReferenceId: {ReferenceId} in branch '{BranchName}'.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    parentBranchName,\n                    parentBranchId,\n                    referenceId,\n                    branchName\n                )\n\n                do!\n                    this\n                        .Clients\n                        .Group($\"{parentBranchId}\")\n                        .NotifyOnSave(branchName, parentBranchName, parentBranchId, referenceId)\n\n                ()\n            }\n            :> Task\n\n        member this.NotifyOnCheckpoint((branchName: BranchName), (parentBranchName: BranchName), (parentBranchId: BranchId), (referenceId: ReferenceId)) =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; Notifying clients with ParentBranch '{ParentBranchName}' ({ParentBranchId}) of checkpoint ReferenceId: {ReferenceId} in branch '{branchName}'.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    parentBranchName,\n                    parentBranchId,\n                    referenceId,\n                    branchName\n                )\n\n                do!\n                    this\n                        .Clients\n                        .Group($\"{parentBranchId}\")\n                        .NotifyOnCheckpoint(branchName, parentBranchName, parentBranchId, referenceId)\n            }\n            :> Task\n\n        member this.NotifyOnCommit((branchName: BranchName), (parentBranchName: BranchName), (parentBranchId: BranchId), (referenceId: ReferenceId)) =\n            task {\n                log.LogInformation(\n                    \"{CurrentInstant}: Node: {HostName}; Notifying clients with ParentBranch '{ParentBranchName}' ({ParentBranchId}) of commit ReferenceId: {ReferenceId} in branch '{branchName}'.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    parentBranchName,\n                    parentBranchId,\n                    referenceId,\n                    branchName\n                )\n\n                do!\n                    this\n                        .Clients\n                        .Group($\"{parentBranchId}\")\n                        .NotifyOnCommit(branchName, parentBranchName, parentBranchId, referenceId)\n            }\n            :> Task\n\n        member this.ServerToClientMessage(message: string) =\n            task {\n                if not <| isNull (this.Clients) then\n                    do! this.Clients.All.ServerToClientMessage(message)\n                else\n                    logToConsole $\"No SignalR clients connected.\"\n            }\n            :> Task\n\n        member this.NotifyAutomationEvent(envelope: AutomationEventEnvelope) =\n            task {\n                if not <| isNull (this.Clients) then\n                    let groupKey =\n                        if envelope.RepositoryId = RepositoryId.Empty then\n                            String.Empty\n                        else\n                            $\"{envelope.RepositoryId}\"\n\n                    if String.IsNullOrWhiteSpace groupKey then\n                        do! this.Clients.All.NotifyAutomationEvent(envelope)\n                    else\n                        do!\n                            this\n                                .Clients\n                                .Group(groupKey)\n                                .NotifyAutomationEvent(envelope)\n                else\n                    logToConsole $\"No SignalR clients connected.\"\n            }\n            :> Task\n\n    let routeAutomationEvent (serviceProvider: IServiceProvider) (envelope: AutomationEventEnvelope) =\n        task {\n            try\n                if isNull serviceProvider then\n                    log.LogWarning(\n                        \"{CurrentInstant}: Node: {HostName}; No service provider available while routing automation event {EventType}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        envelope.EventType\n                    )\n                else\n                    let hubContext = serviceProvider.GetService<IHubContext<NotificationHub, IGraceClientConnection>>()\n\n                    if isNull hubContext then\n                        log.LogWarning(\n                            \"{CurrentInstant}: Node: {HostName}; No SignalR hub context available while routing automation event {EventType}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            envelope.EventType\n                        )\n                    else\n                        let groupKey =\n                            if envelope.RepositoryId = RepositoryId.Empty then\n                                String.Empty\n                            else\n                                $\"{envelope.RepositoryId}\"\n\n                        if String.IsNullOrWhiteSpace groupKey then\n                            do! hubContext.Clients.All.NotifyAutomationEvent(envelope)\n                        else\n                            do!\n                                hubContext\n                                    .Clients\n                                    .Group(groupKey)\n                                    .NotifyAutomationEvent(envelope)\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed routing automation event {EventType}.\",\n                    getCurrentInstantExtended (),\n                    getMachineName,\n                    envelope.CorrelationId,\n                    envelope.EventType\n                )\n        }\n\n    module Subscriber =\n        /// Gets the ReferenceDto for the given ReferenceId.\n        let getReferenceDto referenceId repositoryId correlationId =\n            task {\n                let referenceActorProxy = Reference.CreateActorProxy referenceId repositoryId correlationId\n\n                return! referenceActorProxy.Get correlationId\n            }\n\n        /// Gets the BranchDto for the given BranchId.\n        let getBranchDto branchId repositoryId correlationId =\n            task {\n                let branchActorProxy = Branch.CreateActorProxy branchId repositoryId correlationId\n\n                return! branchActorProxy.Get correlationId\n            }\n\n        let diffTwoDirectoryVersions directoryVersionId1 directoryVersionId2 ownerId organizationId repositoryId correlationId =\n            task {\n                let diffActorProxy = Diff.CreateActorProxy directoryVersionId1 directoryVersionId2 ownerId organizationId repositoryId correlationId\n\n                match! diffActorProxy.Compute correlationId with\n                | Ok result -> return ()\n                | Error graceError ->\n                    log.LogError(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; In Notification.Server.diffTwoDirectoryVersions: Error computing diff between DirectoryVersionId {DirectoryVersionId1} and {DirectoryVersionId2}:\\n{GraceError}\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        directoryVersionId1,\n                        directoryVersionId2,\n                        graceError\n                    )\n\n                    return ()\n            }\n\n        let tryGetRepositoryIdFromMetadata (metadata: EventMetadata) =\n            match metadata.Properties.TryGetValue(nameof RepositoryId) with\n            | true, value ->\n                match Guid.TryParse(value) with\n                | true, repositoryId -> Some repositoryId\n                | _ -> None\n            | _ -> None\n\n        let triggerPromotionSetRecompute (repositoryId: RepositoryId) (promotionSetId: PromotionSetId) (reason: string) (correlationId: CorrelationId) =\n            task {\n                try\n                    let promotionSetActorProxy = PromotionSet.CreateActorProxy promotionSetId repositoryId correlationId\n                    let! exists = promotionSetActorProxy.Exists correlationId\n\n                    if exists then\n                        let recomputeCorrelationId = $\"{correlationId}-recompute-{promotionSetId:N}\"\n                        let metadata = EventMetadata.New recomputeCorrelationId GraceSystemUser\n                        metadata.Properties[ nameof RepositoryId ] <- $\"{repositoryId}\"\n                        metadata.Properties[ \"ActorId\" ] <- $\"{promotionSetId}\"\n\n                        match! promotionSetActorProxy.Handle (Grace.Types.PromotionSet.PromotionSetCommand.RecomputeStepsIfStale(Some reason)) metadata with\n                        | Ok _ -> ()\n                        | Error graceError ->\n                            log.LogWarning(\n                                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed to recompute PromotionSetId {PromotionSetId}: {GraceError}\",\n                                getCurrentInstantExtended (),\n                                getMachineName,\n                                recomputeCorrelationId,\n                                promotionSetId,\n                                graceError\n                            )\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Exception while triggering recompute for PromotionSetId {PromotionSetId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        promotionSetId\n                    )\n            }\n\n        let triggerQueuedPromotionSetRecompute (repositoryId: RepositoryId) (targetBranchId: BranchId) (reason: string) (correlationId: CorrelationId) =\n            task {\n                try\n                    let queueActorProxy = PromotionQueue.CreateActorProxy targetBranchId repositoryId correlationId\n                    let! queueExists = queueActorProxy.Exists correlationId\n\n                    if queueExists then\n                        let! queue = queueActorProxy.Get correlationId\n\n                        let queuedPromotionSetIds =\n                            queue.PromotionSetIds\n                            |> Seq.distinct\n                            |> Seq.toArray\n\n                        let mutable index = 0\n\n                        while index < queuedPromotionSetIds.Length do\n                            do! triggerPromotionSetRecompute repositoryId queuedPromotionSetIds[index] reason correlationId\n                            index <- index + 1\n                with\n                | ex ->\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Exception while scheduling queue recompute for target branch {BranchId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        targetBranchId\n                    )\n            }\n\n        let private parseGuid (value: string) =\n            let mutable parsed = Guid.Empty\n\n            if String.IsNullOrWhiteSpace value |> not\n               && Guid.TryParse(value, &parsed)\n               && parsed <> Guid.Empty then\n                Some parsed\n            else\n                None\n\n        let internal matchesBranchGlob (branchName: BranchName) (branchNameGlob: string) =\n            let normalizedPattern = if String.IsNullOrWhiteSpace branchNameGlob then \"*\" else branchNameGlob.Trim()\n\n            let regexPattern =\n                \"^\"\n                + Regex\n                    .Escape(normalizedPattern)\n                    .Replace(\"\\\\*\", \".*\")\n                + \"$\"\n\n            Regex.IsMatch(\n                $\"{branchName}\",\n                regexPattern,\n                RegexOptions.IgnoreCase\n                ||| RegexOptions.CultureInvariant\n            )\n\n        let private tryGetPromotionSetIdFromMetadata (metadata: EventMetadata) =\n            match metadata.Properties.TryGetValue(\"ActorId\") with\n            | true, actorId -> parseGuid actorId\n            | _ -> None\n\n        let private tryGetTerminalPromotionSetId (links: ReferenceLinkType seq) =\n            links\n            |> Seq.tryPick (fun link ->\n                match link with\n                | ReferenceLinkType.PromotionSetTerminal promotionSetId -> Some promotionSetId\n                | _ -> None)\n\n        let private emitAutomationEvent (hubContext: IHubContext<NotificationHub, IGraceClientConnection>) (envelope: AutomationEventEnvelope) =\n            task {\n                if not <| isNull hubContext then\n                    let groupKey =\n                        if envelope.RepositoryId = RepositoryId.Empty then\n                            String.Empty\n                        else\n                            $\"{envelope.RepositoryId}\"\n\n                    if String.IsNullOrWhiteSpace groupKey then\n                        do! hubContext.Clients.All.NotifyAutomationEvent(envelope)\n                    else\n                        do!\n                            hubContext\n                                .Clients\n                                .Group(groupKey)\n                                .NotifyAutomationEvent(envelope)\n            }\n\n        let private getPromotionSetContext (repositoryId: RepositoryId) (promotionSetId: PromotionSetId) (correlationId: CorrelationId) =\n            task {\n                let promotionSetActorProxy = PromotionSet.CreateActorProxy promotionSetId repositoryId correlationId\n                let! exists = promotionSetActorProxy.Exists correlationId\n\n                if exists then\n                    let! promotionSet = promotionSetActorProxy.Get correlationId\n                    let! branch = getBranchDto promotionSet.TargetBranchId repositoryId correlationId\n                    return Some(promotionSet, branch)\n                else\n                    return None\n            }\n\n        let private tryResolveAutomationBranchContext (graceEvent: GraceEvent) (correlationId: CorrelationId) =\n            task {\n                match graceEvent with\n                | QueueEvent queueEvent ->\n                    match queueEvent.Event with\n                    | PromotionQueueEventType.PromotionSetEnqueued promotionSetId\n                    | PromotionQueueEventType.PromotionSetDequeued promotionSetId ->\n                        match tryGetRepositoryIdFromMetadata queueEvent.Metadata with\n                        | Some repositoryId ->\n                            let! promotionSetContext = getPromotionSetContext repositoryId promotionSetId correlationId\n\n                            match promotionSetContext with\n                            | Some (promotionSet, branch) ->\n                                return\n                                    Some(\n                                        repositoryId,\n                                        branch.BranchId,\n                                        branch.BranchName,\n                                        Some promotionSet.PromotionSetId,\n                                        Some promotionSet.StepsComputationAttempt\n                                    )\n                            | None -> return None\n                        | None -> return None\n                    | _ -> return None\n                | PromotionSetEvent promotionSetEvent ->\n                    match tryGetPromotionSetIdFromMetadata promotionSetEvent.Metadata, tryGetRepositoryIdFromMetadata promotionSetEvent.Metadata with\n                    | Some promotionSetId, Some repositoryId ->\n                        let! promotionSetContext = getPromotionSetContext repositoryId promotionSetId correlationId\n\n                        match promotionSetContext with\n                        | Some (promotionSet, branch) ->\n                            return\n                                Some(\n                                    repositoryId,\n                                    branch.BranchId,\n                                    branch.BranchName,\n                                    Some promotionSet.PromotionSetId,\n                                    Some promotionSet.StepsComputationAttempt\n                                )\n                        | None -> return None\n                    | _ -> return None\n                | ReferenceEvent referenceEvent ->\n                    match referenceEvent.Event with\n                    | ReferenceEventType.Created (_, _, _, repositoryId, branchId, _, _, referenceType, _, links) when referenceType = ReferenceType.Promotion ->\n                        match tryGetTerminalPromotionSetId links with\n                        | Some promotionSetId ->\n                            let! branchDto = getBranchDto branchId repositoryId correlationId\n                            let! promotionSetContext = getPromotionSetContext repositoryId promotionSetId correlationId\n\n                            let stepsComputationAttempt =\n                                promotionSetContext\n                                |> Option.map (fun (promotionSet, _) -> promotionSet.StepsComputationAttempt)\n\n                            return Some(repositoryId, branchId, branchDto.BranchName, Some promotionSetId, stepsComputationAttempt)\n                        | None -> return None\n                    | _ -> return None\n                | _ -> return None\n            }\n\n        let private emitValidationRequestedEvents\n            (hubContext: IHubContext<NotificationHub, IGraceClientConnection>)\n            (sourceEnvelope: AutomationEventEnvelope)\n            (graceEvent: GraceEvent)\n            =\n            task {\n                let correlationId = sourceEnvelope.CorrelationId\n\n                let! context = tryResolveAutomationBranchContext graceEvent correlationId\n\n                match context with\n                | None -> ()\n                | Some (repositoryId, branchId, branchName, promotionSetId, stepsComputationAttempt) ->\n                    let! validationSets = getValidationSets repositoryId 500 false correlationId\n\n                    let matchingValidationSets =\n                        validationSets\n                        |> List.filter (fun validationSet ->\n                            validationSet.Rules\n                            |> List.exists (fun rule ->\n                                rule.EventTypes\n                                |> List.contains sourceEnvelope.EventType\n                                && matchesBranchGlob branchName rule.BranchNameGlob))\n\n                    let mutable index = 0\n\n                    while index < matchingValidationSets.Length do\n                        let validationSet = matchingValidationSets[index]\n                        let mutable validationIndex = 0\n\n                        while validationIndex < validationSet.Validations.Length do\n                            let validation = validationSet.Validations[validationIndex]\n\n                            match validation.ExecutionMode with\n                            | ValidationExecutionMode.AsyncCallback ->\n                                let payload =\n                                    {|\n                                        validationSetId = validationSet.ValidationSetId\n                                        promotionSetId = promotionSetId\n                                        stepsComputationAttempt = stepsComputationAttempt\n                                        targetBranchId = branchId\n                                        targetBranchName = branchName\n                                        validationName = validation.Name\n                                        validationVersion = validation.Version\n                                        sourceEventType = sourceEnvelope.EventType\n                                    |}\n\n                                let validationRequestedEnvelope =\n                                    AutomationEventEnvelope.Create\n                                        AutomationEventType.ValidationRequested\n                                        (getCurrentInstant ())\n                                        correlationId\n                                        validationSet.OwnerId\n                                        validationSet.OrganizationId\n                                        validationSet.RepositoryId\n                                        $\"{validationSet.ValidationSetId}\"\n                                        (serialize payload)\n\n                                do! emitAutomationEvent hubContext validationRequestedEnvelope\n                            | ValidationExecutionMode.Synchronous ->\n                                let validationResultId = Guid.NewGuid()\n                                let validationResultActorProxy = ValidationResult.CreateActorProxy validationResultId repositoryId correlationId\n                                let metadata = EventMetadata.New correlationId GraceSystemUser\n                                metadata.Properties[ nameof RepositoryId ] <- $\"{repositoryId}\"\n                                metadata.Properties[ \"ActorId\" ] <- $\"{validationResultId}\"\n\n                                let validationResultDto =\n                                    { ValidationResultDto.Default with\n                                        ValidationResultId = validationResultId\n                                        OwnerId = validationSet.OwnerId\n                                        OrganizationId = validationSet.OrganizationId\n                                        RepositoryId = repositoryId\n                                        ValidationSetId = Some validationSet.ValidationSetId\n                                        PromotionSetId = promotionSetId\n                                        StepsComputationAttempt = stepsComputationAttempt\n                                        ValidationName = validation.Name\n                                        ValidationVersion = validation.Version\n                                        Output =\n                                            {\n                                                Status = ValidationStatus.Pass\n                                                Summary = $\"Synchronous validation '{validation.Name}' recorded automatically from {sourceEnvelope.EventType}.\"\n                                                ArtifactIds = []\n                                            }\n                                        OnBehalfOf = [ UserId GraceSystemUser ]\n                                        CreatedAt = getCurrentInstant ()\n                                    }\n\n                                match! validationResultActorProxy.Handle (ValidationResultCommand.Record validationResultDto) metadata with\n                                | Ok _ -> ()\n                                | Error graceError ->\n                                    log.LogWarning(\n                                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Failed recording synchronous validation result for ValidationSetId {ValidationSetId}. Error: {GraceError}\",\n                                        getCurrentInstantExtended (),\n                                        getMachineName,\n                                        correlationId,\n                                        validationSet.ValidationSetId,\n                                        graceError\n                                    )\n\n                            validationIndex <- validationIndex + 1\n\n                        index <- index + 1\n            }\n\n        let hubContext = lazy (serviceProvider.GetService<IHubContext<NotificationHub, IGraceClientConnection>>())\n\n        //let private getHubContextOld () =\n        //    if isNull hubContext then\n        //        if isNull serviceProvider then\n        //            log.LogWarning(\"NotificationHub context requested before the service provider was initialized.\")\n        //        else\n        //            hubContext <- serviceProvider.GetService<IHubContext<NotificationHub, IGraceClientConnection>>()\n\n        //            if isNull hubContext then\n        //                log.LogWarning(\"NotificationHub context could not be resolved from the service provider.\")\n\n        //    hubContext\n\n        //let private getHubContext () =\n        //    if isNull hubContext then\n        //        hubContext <- serviceProvider.GetService<IHubContext<NotificationHub, IGraceClientConnection>>()\n\n        //        if isNull hubContext then\n        //            log.LogWarning(\"NotificationHub context could not be resolved from the service provider.\")\n\n        //    hubContext\n\n        /// Main processing for asynchronous event notifications received from the pub-sub system.\n        let handleEvent (graceEvent: GraceEvent) =\n            task {\n                let hubContext = hubContext.Value\n\n                match graceEvent with\n                | BranchEvent branchEvent ->\n                    let correlationId = branchEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received BranchEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | DirectoryVersionEvent directoryVersionEvent ->\n                    let correlationId = directoryVersionEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received DirectoryVersionEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | OrganizationEvent organizationEvent ->\n                    let correlationId = organizationEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received OrganizationEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | OwnerEvent ownerEvent ->\n                    let correlationId = ownerEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received OwnerEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | ReferenceEvent referenceEvent ->\n                    let correlationId = referenceEvent.Metadata.CorrelationId\n                    let repositoryId = Guid.Parse($\"{referenceEvent.Metadata.Properties[nameof RepositoryId]}\")\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received ReferenceEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n\n                    do! DerivedComputation.handleReferenceEvent referenceEvent\n\n                    match referenceEvent.Event with\n                    | ReferenceEventType.Created (referenceId,\n                                                  ownerId,\n                                                  organizationId,\n                                                  repositoryId,\n                                                  branchId,\n                                                  directoryId,\n                                                  sha256Hash,\n                                                  referenceType,\n                                                  referenceText,\n                                                  links) ->\n                        match referenceType with\n                        | ReferenceType.Promotion ->\n                            let! branchDto = getBranchDto branchId repositoryId correlationId\n\n                            let isTerminalPromotion =\n                                links\n                                |> Seq.exists (fun link ->\n                                    match link with\n                                    | ReferenceLinkType.PromotionSetTerminal _ -> true\n                                    | _ -> false)\n\n                            if isTerminalPromotion then\n                                do!\n                                    triggerQueuedPromotionSetRecompute\n                                        repositoryId\n                                        branchId\n                                        $\"Target branch advanced to terminal promotion {referenceId}.\"\n                                        correlationId\n\n                            // Create the diff between the new promotion and previous promotion.\n                            let! latestTwoPromotions = getPromotions repositoryId branchId 2 correlationId\n\n                            if latestTwoPromotions.Length = 2 then\n                                do!\n                                    diffTwoDirectoryVersions\n                                        latestTwoPromotions[0].DirectoryId\n                                        latestTwoPromotions[1].DirectoryId\n                                        branchDto.OwnerId\n                                        branchDto.OrganizationId\n                                        branchDto.RepositoryId\n                                        correlationId\n\n                        | ReferenceType.Commit ->\n                            let! branchDto = getBranchDto branchId repositoryId correlationId\n                            let! parentBranchDto = getBranchDto branchDto.ParentBranchId repositoryId correlationId\n\n                            if not <| isNull hubContext then\n                                do!\n                                    hubContext\n                                        .Clients\n                                        .Group($\"{branchDto.ParentBranchId}\")\n                                        .NotifyOnCommit(branchDto.BranchName, parentBranchDto.BranchName, parentBranchDto.ParentBranchId, referenceId)\n                            else\n                                log.LogWarning(\"No SignalR hub context available; cannot notify clients of commit.\")\n\n                            let directoryVersionActorProxy = DirectoryVersion.CreateActorProxy directoryId repositoryId correlationId\n                            let! exists = directoryVersionActorProxy.Exists correlationId\n\n                            if exists then\n                                // Create the zip file for this directory version.\n                                let! zipFileUri = directoryVersionActorProxy.GetZipFileUri correlationId\n\n                                // Create the diff between the new commit and the previous commit.\n                                let! latestTwoCommits = getCommits repositoryId branchId 2 correlationId\n\n                                if latestTwoCommits.Length = 2 then\n                                    do!\n                                        diffTwoDirectoryVersions\n                                            latestTwoCommits[0].DirectoryId\n                                            latestTwoCommits[1].DirectoryId\n                                            branchDto.OwnerId\n                                            branchDto.OrganizationId\n                                            branchDto.RepositoryId\n                                            correlationId\n\n                                // Create the diff between the commit and the parent branch's most recent promotion.\n                                match! getLatestPromotion branchDto.RepositoryId branchDto.ParentBranchId with\n                                | Some latestPromotion ->\n                                    do!\n                                        diffTwoDirectoryVersions\n                                            directoryId\n                                            latestPromotion.DirectoryId\n                                            branchDto.OwnerId\n                                            branchDto.OrganizationId\n                                            branchDto.RepositoryId\n                                            correlationId\n                                | None -> ()\n                        | ReferenceType.Checkpoint ->\n                            let! branchDto = getBranchDto branchId repositoryId correlationId\n                            let! parentBranchDto = getBranchDto branchDto.ParentBranchId repositoryId correlationId\n\n                            if not <| isNull hubContext then\n                                do!\n                                    hubContext\n                                        .Clients\n                                        .Group($\"{branchDto.ParentBranchId}\")\n                                        .NotifyOnCheckpoint(branchDto.BranchName, parentBranchDto.BranchName, parentBranchDto.ParentBranchId, referenceId)\n\n                            // Create the diff between the two most recent checkpoints.\n                            let! checkpoints = getCheckpoints repositoryId branchId 2 correlationId\n\n                            if checkpoints.Length = 2 then\n                                do!\n                                    diffTwoDirectoryVersions\n                                        checkpoints[0].DirectoryId\n                                        checkpoints[1].DirectoryId\n                                        branchDto.OwnerId\n                                        branchDto.OrganizationId\n                                        branchDto.RepositoryId\n                                        correlationId\n\n                            // Create a diff between the checkpoint and the most recent commit.\n                            match! getLatestCommit repositoryId branchId with\n                            | Some latestCommit ->\n                                do!\n                                    diffTwoDirectoryVersions\n                                        directoryId\n                                        latestCommit.DirectoryId\n                                        branchDto.OwnerId\n                                        branchDto.OrganizationId\n                                        branchDto.RepositoryId\n                                        correlationId\n                            | None -> ()\n\n                        | ReferenceType.Save ->\n                            let! branchDto = getBranchDto branchId repositoryId correlationId\n                            let! parentBranchDto = getBranchDto branchDto.ParentBranchId repositoryId correlationId\n\n                            if not <| isNull hubContext then\n                                do!\n                                    hubContext\n                                        .Clients\n                                        .Group($\"{branchDto.ParentBranchId}\")\n                                        .NotifyOnSave(branchDto.BranchName, parentBranchDto.BranchName, parentBranchDto.ParentBranchId, referenceId)\n                            else\n                                log.LogWarning(\"No SignalR hub context available; cannot notify clients of save.\")\n\n                            // Create the diff between the new save and the previous save.\n                            let! latestTwoSaves = getSaves branchDto.RepositoryId branchId 2 correlationId\n\n                            if latestTwoSaves.Length = 2 then\n                                do!\n                                    diffTwoDirectoryVersions\n                                        latestTwoSaves[0].DirectoryId\n                                        latestTwoSaves[1].DirectoryId\n                                        branchDto.OwnerId\n                                        branchDto.OrganizationId\n                                        branchDto.RepositoryId\n                                        correlationId\n\n                            // Create the diff between the new save and the most recent commit.\n                            let mutable latestCommit = Reference.ReferenceDto.Default\n\n                            match! getLatestCommit branchDto.RepositoryId branchDto.BranchId with\n                            | Some latest ->\n                                latestCommit <- latest\n\n                                do!\n                                    diffTwoDirectoryVersions\n                                        latestCommit.DirectoryId\n                                        directoryId\n                                        branchDto.OwnerId\n                                        branchDto.OrganizationId\n                                        branchDto.RepositoryId\n                                        correlationId\n                            | None -> ()\n\n                            // Create the diff between the new save and the most recent checkpoint,\n                            //   if the checkpoint is newer than the most recent commit.\n                            match! getLatestCheckpoint branchDto.RepositoryId branchDto.BranchId with\n                            | Some latestCheckpoint ->\n                                if latestCheckpoint.CreatedAt > latestCommit.CreatedAt then\n                                    do!\n                                        diffTwoDirectoryVersions\n                                            latestCheckpoint.DirectoryId\n                                            directoryId\n                                            branchDto.OwnerId\n                                            branchDto.OrganizationId\n                                            branchDto.RepositoryId\n                                            correlationId\n                            | None -> ()\n\n                        | ReferenceType.Tag\n                        | ReferenceType.Rebase\n                        | ReferenceType.External -> ()\n\n                        do!\n                            hubContext\n                                .Clients\n                                .Group($\"{repositoryId}\")\n                                .NotifyRepository(repositoryId, referenceId)\n                    | _ -> ()\n                | RepositoryEvent repositoryEvent ->\n                    let correlationId = repositoryEvent.Metadata.CorrelationId\n\n                    logToConsole\n                        $\"Received RepositoryEvent: {getDiscriminatedUnionFullName repositoryEvent.Event} {Environment.NewLine}{repositoryEvent.Metadata}\"\n                | PolicyEvent policyEvent ->\n                    let correlationId = policyEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received PolicyEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n\n                    do! DerivedComputation.handlePolicyEvent policyEvent\n                | WorkItemEvent workItemEvent ->\n                    let correlationId = workItemEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received WorkItemEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | ReviewEvent reviewEvent ->\n                    let correlationId = reviewEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received ReviewEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | QueueEvent queueEvent ->\n                    let correlationId = queueEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received QueueEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n\n                    match queueEvent.Event with\n                    | Grace.Types.Queue.PromotionQueueEventType.PromotionSetEnqueued promotionSetId ->\n                        match tryGetRepositoryIdFromMetadata queueEvent.Metadata with\n                        | Some repositoryId -> do! triggerPromotionSetRecompute repositoryId promotionSetId \"PromotionSet enqueued.\" correlationId\n                        | None -> ()\n                    | _ -> ()\n                | PromotionSetEvent promotionSetEvent ->\n                    let correlationId = promotionSetEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received PromotionSetEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | ValidationSetEvent validationSetEvent ->\n                    let correlationId = validationSetEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received ValidationSetEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | ValidationResultEvent validationResultEvent ->\n                    let correlationId = validationResultEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received ValidationResultEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n                | ArtifactEvent artifactEvent ->\n                    let correlationId = artifactEvent.Metadata.CorrelationId\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Received ArtifactEvent notification.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId\n                    )\n\n                match EventingPublisher.tryCreateEnvelope graceEvent with\n                | Some envelope ->\n                    do! emitAutomationEvent hubContext envelope\n\n                    if envelope.EventType = AutomationEventType.PromotionSetStepsUpdated then\n                        let recomputeSucceededEnvelope =\n                            { envelope with\n                                EventId = Guid.NewGuid()\n                                EventType = AutomationEventType.PromotionSetRecomputeSucceeded\n                                EventTime = getCurrentInstant ()\n                            }\n\n                        do! emitAutomationEvent hubContext recomputeSucceededEnvelope\n\n                    do! emitValidationRequestedEvents hubContext envelope graceEvent\n                | None -> ()\n\n            //return! setStatusCode StatusCodes.Status204NoContent next context\n            }\n\n        type GraceEventSubscriptionService(loggerFactory: ILoggerFactory) =\n            let subscriptionLog = loggerFactory.CreateLogger(\"Notification.Server.Subscription\")\n            let credential = lazy (DefaultAzureCredential())\n            let mutable client: ServiceBusClient option = None\n            let mutable processor: ServiceBusProcessor option = None\n\n            let handleProcessorError (args: ProcessErrorEventArgs) =\n                task {\n                    //subscriptionLog.LogError(\n                    //    args.Exception,\n                    //    \"Grace pub-sub processor fault. ErrorSource: {ErrorSource}; EntityPath: {EntityPath}.\",\n                    //    args.ErrorSource,\n                    //    args.EntityPath\n                    //)\n\n                    subscriptionLog.LogWarning(\"Azure Service Bus not ready; pausing for five seconds to retry.\")\n                    do! Task.Delay(TimeSpan.FromSeconds(5.0))\n                }\n                :> Task\n\n            let processGraceEvent (args: ProcessMessageEventArgs) =\n                task {\n                    try\n                        use bodyStream = args.Message.Body.ToStream()\n                        let graceEvent = JsonSerializer.Deserialize<GraceEvent>(bodyStream, options = Constants.JsonSerializerOptions)\n\n                        do! handleEvent graceEvent\n                        do! args.CompleteMessageAsync(args.Message, args.CancellationToken)\n                    with\n                    | ex ->\n                        subscriptionLog.LogError(\n                            ex,\n                            \"Failed to process GraceEvent message {MessageId} (CorrelationId: {CorrelationId}).\",\n                            args.Message.MessageId,\n                            args.Message.CorrelationId\n                        )\n\n                        do! args.AbandonMessageAsync(args.Message, cancellationToken = args.CancellationToken)\n                }\n\n            let startAzureServiceBusProcessor (settings: AzureServiceBusPubSubSettings) (cancellationToken: CancellationToken) =\n                task {\n                    if processor.IsSome then\n                        subscriptionLog.LogDebug(\"Grace pub-sub listener already running; skipping duplicate startup.\")\n                    else\n                        let mutable ready = false\n\n                        while not ready\n                              && not cancellationToken.IsCancellationRequested do\n                            try\n                                let serviceBusClient =\n                                    if settings.UseManagedIdentity then\n                                        let fullyQualifiedNamespace =\n                                            if not (String.IsNullOrWhiteSpace settings.FullyQualifiedNamespace) then\n                                                settings.FullyQualifiedNamespace\n                                            else\n                                                AzureEnvironment.tryGetServiceBusFullyQualifiedNamespace ()\n                                                |> Option.defaultWith (fun () ->\n                                                    invalidOp \"Azure Service Bus namespace must be configured when using a managed identity.\")\n\n                                        ServiceBusClient(fullyQualifiedNamespace, defaultAzureCredential.Value)\n                                    else\n                                        ServiceBusClient(settings.ConnectionString)\n\n                                let serviceBusProcessorOptions =\n                                    ServiceBusProcessorOptions(\n                                        AutoCompleteMessages = false,\n                                        MaxConcurrentCalls = 4,\n                                        PrefetchCount = 16,\n                                        Identifier = Environment.MachineName\n                                    )\n\n                                let serviceBusProcessor =\n                                    serviceBusClient.CreateProcessor(settings.TopicName, settings.SubscriptionName, serviceBusProcessorOptions)\n\n                                serviceBusProcessor.add_ProcessMessageAsync (Func<ProcessMessageEventArgs, Task>(fun args -> processGraceEvent args))\n                                serviceBusProcessor.add_ProcessErrorAsync (Func<ProcessErrorEventArgs, Task>(fun args -> handleProcessorError args))\n\n                                do! serviceBusProcessor.StartProcessingAsync(cancellationToken)\n\n                                client <- Some serviceBusClient\n                                processor <- Some serviceBusProcessor\n\n                                subscriptionLog.LogInformation(\n                                    \"Started Grace pub-sub listener for topic {TopicName} / subscription {SubscriptionName}.\",\n                                    settings.TopicName,\n                                    settings.SubscriptionName\n                                )\n\n                                ready <- true\n                            with\n                            | ex ->\n                                subscriptionLog.LogWarning(ex, \"Azure Service Bus not ready; pausing for five seconds to retry.\")\n                                do! Task.Delay(TimeSpan.FromSeconds(5.0), cancellationToken)\n                }\n\n            let stopAzureServiceBusProcessor cancellationToken =\n                task {\n                    match processor with\n                    | Some proc ->\n                        try\n                            do! proc.StopProcessingAsync(cancellationToken)\n                        with\n                        | ex -> subscriptionLog.LogWarning(ex, \"Grace pub-sub processor stop failed; continuing with Dispose().\")\n\n                        do! proc.DisposeAsync()\n                        processor <- None\n                    | None -> ()\n\n                    match client with\n                    | Some clientInstance ->\n                        do! clientInstance.DisposeAsync()\n                        client <- None\n                    | None -> ()\n                }\n\n            let startSubscriber (cancellationToken: CancellationToken) : Task =\n                match pubSubSettings with\n                | { System = GracePubSubSystem.AzureServiceBus; AzureServiceBus = Some settings } -> startAzureServiceBusProcessor settings cancellationToken\n                | { System = GracePubSubSystem.AzureServiceBus; AzureServiceBus = None } ->\n                    subscriptionLog.LogWarning(\"Azure Service Bus pub-sub selected but settings were missing; skipping notification subscriber startup.\")\n\n                    Task.CompletedTask\n                | { System = GracePubSubSystem.UnknownPubSubProvider } ->\n                    subscriptionLog.LogInformation(\"Grace pub-sub disabled; notification subscriber will not start.\")\n                    Task.CompletedTask\n                | otherSettings ->\n                    subscriptionLog.LogWarning(\"Grace pub-sub system {System} is not supported for the notification subscriber.\", otherSettings.System)\n\n                    Task.CompletedTask\n\n            interface IHostedService with\n                member _.StartAsync(cancellationToken: CancellationToken) = startSubscriber cancellationToken\n\n                member _.StopAsync(cancellationToken: CancellationToken) = task { do! stopAzureServiceBusProcessor cancellationToken }\n"
  },
  {
    "path": "src/Grace.Server/Organization.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Organization\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Organization\nopen Grace.Types.Types\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule Organization =\n\n    type Validations<'T when 'T :> OrganizationParameters> = 'T -> ValueTask<Result<unit, OrganizationError>> array\n\n    let activitySource = new ActivitySource(\"Organization\")\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Organization.Server\")\n\n    let processCommand<'T when 'T :> OrganizationParameters>\n        (context: HttpContext)\n        (validations: Validations<'T>)\n        (command: 'T -> ValueTask<OrganizationCommand>)\n        =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let commandName = context.Items[\"Command\"] :?> string\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                // We know these Id's from ValidateIds.Middleware, so let's set them so we never have to resolve them again.\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n\n                let handleCommand (organizationId: string) cmd =\n                    task {\n                        let organizationGuid = Guid.Parse(organizationId)\n                        let actorProxy = Organization.CreateActorProxy organizationGuid correlationId\n\n                        match! actorProxy.Handle cmd (createMetadata context) with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            log.LogDebug(\n                                \"{CurrentInstant}: In Branch.Server.handleCommand: error from actorProxy.Handle: {error}\",\n                                getCurrentInstantExtended (),\n                                (graceError.ToString())\n                            )\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                log.LogDebug(\n                    \"{CurrentInstant}: In Organization.Server.processCommand: validationsPassed: {validationsPassed}.\",\n                    getCurrentInstantExtended (),\n                    validationsPassed\n                )\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let! result = handleCommand graceIds.OrganizationIdString cmd\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Finished {path}; Status code: {statusCode}; OwnerId: {ownerId}; OrganizationId: {organizationId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        context.Request.Path,\n                        context.Response.StatusCode,\n                        graceIds.OwnerIdString,\n                        graceIds.OrganizationIdString\n                    )\n\n                    return result\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = OrganizationError.getErrorMessage error\n                    log.LogDebug(\"{CurrentInstant}: error: {error}\", getCurrentInstantExtended (), errorMessage)\n\n                    let graceError =\n                        (GraceError.Create errorMessage (getCorrelationId context))\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(\"Command\", commandName)\n                            .enhance(\"Path\", context.Request.Path.Value)\n                            .enhance (\"Error\", errorMessage)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Organization.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    (getCorrelationId context)\n                )\n\n                let graceError =\n                    (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Generic processor for all Organization queries.\n    let processQuery<'T, 'U when 'T :> OrganizationParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (maxCount: int)\n        (query: QueryResult<IOrganizationActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    // Get the actor proxy for this organization.\n                    let organizationGuid = Guid.Parse(graceIds.OrganizationIdString)\n                    let actorProxy = Organization.CreateActorProxy organizationGuid correlationId\n\n                    // Execute the query.\n                    let! queryResult = query context maxCount actorProxy\n\n                    // Wrap the result in a GraceReturnValue.\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError =\n                        (GraceError.Create (OrganizationError.getErrorMessage error) correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.Create $\"{ExceptionResponse.Create ex}\" correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Create an organization.\n    let Create: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: CreateOrganizationParameters) =\n                    [|\n                        Organization.organizationDoesNotExist\n                            parameters.OwnerId\n                            parameters.OwnerName\n                            parameters.OrganizationId\n                            parameters.OrganizationName\n                            parameters.CorrelationId\n                            OrganizationIdAlreadyExists\n                        Organization.organizationNameIsUniqueWithinOwner\n                            parameters.OwnerId\n                            parameters.OwnerName\n                            parameters.OrganizationName\n                            context\n                            parameters.CorrelationId\n                            OrganizationNameAlreadyExists\n                    |]\n\n                let command (parameters: CreateOrganizationParameters) =\n                    task {\n                        let ownerIdGuid = Guid.Parse(parameters.OwnerId)\n                        let organizationIdGuid = Guid.Parse(parameters.OrganizationId)\n                        return Create(organizationIdGuid, OrganizationName parameters.OrganizationName, ownerIdGuid)\n                    }\n                    |> ValueTask<OrganizationCommand>\n\n                log.LogDebug(\"{CurrentInstant}: In Grace.Server.Create.\", getCurrentInstantExtended ())\n                context.Items.Add(\"Command\", nameof Create)\n                return! processCommand context validations command\n            }\n\n    /// Set the name of an organization.\n    let SetName: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOrganizationNameParameters) =\n                    [|\n                        String.isNotEmpty parameters.NewName OrganizationError.OrganizationNameIsRequired\n                        String.isValidGraceName parameters.NewName OrganizationError.InvalidOrganizationName\n                        Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationError.OrganizationIsDeleted\n                    |]\n\n                let command (parameters: SetOrganizationNameParameters) =\n                    SetName(OrganizationName parameters.NewName)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetName)\n                return! processCommand context validations command\n            }\n\n    /// Set the type of an organization (Public, Private).\n    let SetType: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOrganizationTypeParameters) =\n                    [|\n                        DiscriminatedUnion.isMemberOf<OrganizationType, OrganizationError> parameters.OrganizationType OrganizationError.InvalidOrganizationType\n                        Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationError.OrganizationIsDeleted\n                    |]\n\n                let command (parameters: SetOrganizationTypeParameters) =\n                    SetType(\n                        discriminatedUnionFromString<OrganizationType>(\n                            parameters.OrganizationType\n                        )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetType)\n                return! processCommand context validations command\n            }\n\n    /// Set the search visibility of an organization (Visible, Hidden).\n    let SetSearchVisibility: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOrganizationSearchVisibilityParameters) =\n                    [|\n                        String.isNotEmpty parameters.SearchVisibility SearchVisibilityIsRequired\n                        DiscriminatedUnion.isMemberOf<SearchVisibility, OrganizationError> parameters.SearchVisibility OrganizationError.InvalidSearchVisibility\n                        Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationError.OrganizationIsDeleted\n                    |]\n\n                let command (parameters: SetOrganizationSearchVisibilityParameters) =\n                    SetSearchVisibility(\n                        discriminatedUnionFromString<SearchVisibility>(\n                            parameters.SearchVisibility\n                        )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetSearchVisibility)\n                return! processCommand context validations command\n            }\n\n    /// Set the description of an organization.\n    let SetDescription: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOrganizationDescriptionParameters) =\n                    [|\n                        String.isNotEmpty parameters.Description OrganizationError.DescriptionIsRequired\n                        String.maxLength parameters.Description 2048 OrganizationError.DescriptionIsTooLong\n                        Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationError.OrganizationIsDeleted\n                    |]\n\n                let command (parameters: SetOrganizationDescriptionParameters) =\n                    SetDescription(parameters.Description)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetDescription)\n                return! processCommand context validations command\n            }\n\n    /// List the repositories of an organization.\n    let ListRepositories: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                try\n                    let validations (parameters: ListRepositoriesParameters) =\n                        [|\n                            Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationIsDeleted\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IOrganizationActor) =\n                        task {\n                            let! repositories = actorProxy.ListRepositories(getCorrelationId context)\n                            return! context.WriteJsonAsync(repositories)\n                        }\n\n                    let! parameters = context |> parse<ListRepositoriesParameters>\n                    return! processQuery context parameters validations 1 query\n                with\n                | ex ->\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Delete an organization.\n    let Delete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: DeleteOrganizationParameters) =\n                    [|\n                        String.isNotEmpty parameters.DeleteReason OrganizationError.DeleteReasonIsRequired\n                        Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationError.OrganizationIsDeleted\n                    |]\n\n                let command (parameters: DeleteOrganizationParameters) =\n                    DeleteLogical(parameters.Force, parameters.DeleteReason)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof DeleteLogical)\n                return! processCommand context validations command\n            }\n\n    /// Undelete a previous-deleted organization.\n    let Undelete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: OrganizationParameters) =\n                    [|\n                        Organization.organizationIsDeleted context parameters.CorrelationId OrganizationIsNotDeleted\n                    |]\n\n                let command (parameters: OrganizationParameters) = Undelete |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Undelete)\n                return! processCommand context validations command\n            }\n\n    /// Get an organization.\n    let Get: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetOrganizationParameters) =\n                        [|\n                            Organization.organizationIsNotDeleted context parameters.CorrelationId OrganizationIsDeleted\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IOrganizationActor) =\n                        task { return! actorProxy.Get(getCorrelationId context) }\n\n                    let! parameters = context |> parse<GetOrganizationParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; OwnerId: {ownerId}; OrganizationId: {organizationId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.OwnerIdString,\n                        graceIds.OrganizationIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    let graceError =\n                        (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result500ServerError graceError\n            }\n"
  },
  {
    "path": "src/Grace.Server/OrleansFilters.Server.fs",
    "content": "namespace Grace.Server\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Microsoft.Extensions.Logging\nopen Orleans\nopen Orleans.Hosting\nopen Orleans.Persistence\nopen Orleans.Runtime\nopen System\nopen System.Diagnostics\nopen System.Linq\nopen System.Threading.Tasks\nopen FSharpPlus.Data\nopen System.Collections.Generic\nopen Grace.Shared.Parameters\n\nmodule Orleans =\n\n    /// Provides partition keys for grains based on their type and context when Orleans is used with Cosmos DB.\n    type GracePartitionKeyProvider() =\n        let log = loggerFactory.CreateLogger(\"OrleansFilters.Server\")\n\n        interface Cosmos.IPartitionKeyProvider with\n            member _.GetPartitionKey(grainType: string, grainId: GrainId) =\n                ValueTask<string>(\n                    task {\n                        //logToConsole $\"****GracePartitionKeyProvider: grainType: {grainType}; grainId: {grainId}.\"\n\n                        let orleansContext =\n                            match memoryCache.GetOrleansContextEntry(grainId) with\n                            | Some orleansContext -> orleansContext\n                            | None -> Dictionary<string, obj>()\n\n                        //orleansContext\n                        //|> Seq.iter (fun kvp -> logToConsole $\"**** - {kvp.Key}: {kvp.Value}\")\n\n                        let organizationId () = $\"{orleansContext[nameof OrganizationId]}\"\n                        let repositoryId () = $\"{orleansContext[nameof RepositoryId]}\"\n\n                        let partitionKey =\n                            match grainType with\n                            | StateName.AccessControl -> grainId.Key.ToString()\n                            | StateName.Branch -> repositoryId ()\n                            | StateName.Diff -> repositoryId ()\n                            | StateName.DirectoryAppearance -> repositoryId ()\n                            | StateName.DirectoryVersion -> repositoryId ()\n                            | StateName.FileAppearance -> repositoryId ()\n                            | StateName.NamedSection -> repositoryId ()\n                            | StateName.Organization -> StateName.Organization\n                            | StateName.Owner -> StateName.Owner\n                            | StateName.PersonalAccessToken -> grainId.Key.ToString()\n                            | StateName.Policy -> repositoryId ()\n                            | StateName.PromotionSet -> repositoryId ()\n                            | StateName.PromotionQueue -> repositoryId ()\n                            | StateName.ValidationSet -> repositoryId ()\n                            | StateName.ValidationResult -> repositoryId ()\n                            | StateName.Artifact -> repositoryId ()\n                            | StateName.Reference -> repositoryId ()\n                            | StateName.Reminder -> StateName.Reminder\n                            | StateName.Repository -> organizationId ()\n                            | StateName.RepositoryPermission -> repositoryId ()\n                            | StateName.Review -> repositoryId ()\n                            | StateName.User -> StateName.User\n                            | StateName.WorkItem -> repositoryId ()\n                            | StateName.WorkItemNumberCounter -> repositoryId ()\n                            | _ ->\n                                raise (\n                                    ArgumentException(\n                                        $\"In {typeof<GracePartitionKeyProvider>.Name}.GetPartitionKey: No partition key assigned for grain type: {grainType}.\"\n                                    )\n                                )\n\n                                String.Empty\n\n                        let correlationid = getCorrelationId ()\n\n                        //logToConsole\n                        //    $\"****GracePartitionKeyProvider: correlationId: {correlationid}; grainType: {grainType}; grainId: {grainId}; partitionKey: {partitionKey}.\"\n\n                        return partitionKey\n                    }\n                )\n\n    /// Centralizes pre‑ and post‑invoke behavior for all grains.\n    type CorrelationLoggingFilter() =\n        let log = loggerFactory.CreateLogger(\"OrleansFilters.Server\")\n\n        interface IIncomingGrainCallFilter with\n            member _.Invoke(context: IIncomingGrainCallContext) =\n                task {\n                    let correlationId = getCorrelationId ()\n                    let actorName = getActorName ()\n\n                    let sb = stringBuilderPool.Get()\n\n                    if not <| isNull (RequestContext.Entries) then\n                        RequestContext.Keys\n                        |> Seq.iter (fun key ->\n                            logToConsole $\"RequestContext: {key}\"\n\n                            let blah =\n                                match RequestContext.Get(key) with\n                                | :? string as s -> s\n                                | _ -> String.Empty\n\n                            sb.Append($\"{key} = {blah}; \") |> ignore)\n\n                        log.LogInformation(\"RequestContext: {RequestContext}\", sb)\n                        stringBuilderPool.Return(sb)\n\n                    let grainType = $\"{context.TargetId.Type}\"\n                    let grainId = $\"{context.TargetId.Key}\"\n                    let log = loggerFactory.CreateLogger(grainType)\n                    use _scope = log.BeginScope(\"Grain {GrainType}/{GrainId}\", grainType, grainId)\n\n                    log.LogTrace(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Reminder {ActorName}.{MethodName}; GrainId: {Id}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        actorName,\n                        context.InterfaceMethod.Name,\n                        grainId\n                    )\n\n                    let actorStartTime = getCurrentInstant ()\n\n                    try\n                        // Invoke the grain method.\n                        do! context.Invoke()\n                    with\n                    | ex ->\n                        // Log the exception if it occurs during the grain method invocation.\n                        let duration_ms = getDurationRightAligned_ms actorStartTime\n\n                        log.LogError(\n                            ex,\n                            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Exception in {ActorName}.{MethodName}; GrainId: {Id}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            duration_ms,\n                            correlationId,\n                            actorName,\n                            context.InterfaceMethod.Name,\n                            grainId\n                        )\n\n                    let command = getCurrentCommand ()\n\n                    let duration_ms = getDurationRightAligned_ms actorStartTime\n                    //if not <| String.IsNullOrEmpty(actorName) then\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {ActorName}.{MethodName};{Command} GrainId: {Id}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        correlationId,\n                        actorName,\n                        context.InterfaceMethod.Name,\n                        command,\n                        grainId\n                    )\n                }\n"
  },
  {
    "path": "src/Grace.Server/Owner.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Types.Owner\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Owner\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Linq\nopen System.Threading.Tasks\n\nmodule Owner =\n\n    type Validations<'T when 'T :> OwnerParameters> = 'T -> ValueTask<Result<unit, OwnerError>> array\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Owner.Server\")\n\n    let activitySource = new ActivitySource(\"Owner\")\n\n    /// Generic processor for all Owner commands.\n    let processCommand<'T when 'T :> OwnerParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<OwnerCommand>) =\n        task {\n            let commandName = context.Items[\"Command\"] :?> string\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                // We know these Id's from ValidateIds.Middleware, so let's set them so we never have to resolve them again.\n                parameters.OwnerId <- graceIds.OwnerIdString\n\n                let handleCommand (ownerId: string) cmd =\n                    task {\n                        let t = cmd.GetType()\n\n                        let isSupported =\n                            not <| (isNull t.Namespace)\n                            && t.Namespace.StartsWith(\"Grace\", StringComparison.InvariantCulture)\n\n                        let ownerGuid = Guid.Parse(ownerId)\n                        let actorProxy = Owner.CreateActorProxy ownerGuid (getCorrelationId context)\n                        let metadata = createMetadata context\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Owner.Server sending command {Command} to actor. CorrelationId: {CorrelationId}; OwnerId: {OwnerId}.\",\n                            getCurrentInstantExtended (),\n                            getDiscriminatedUnionCaseName cmd,\n                            metadata.CorrelationId,\n                            ownerId\n                        )\n\n                        match! actorProxy.Handle cmd metadata with\n                        | Ok graceReturnValue ->\n                            log.LogInformation(\n                                \"{CurrentInstant}: Owner.Server received command result. CorrelationId: {CorrelationId}; OwnerId: {OwnerId}.\",\n                                getCurrentInstantExtended (),\n                                metadata.CorrelationId,\n                                ownerId\n                            )\n\n                            graceReturnValue\n                                .enhance(parameterDictionary :> IReadOnlyDictionary<string, obj>)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            log.LogDebug(\n                                \"{CurrentInstant}: In Branch.Server.handleCommand: error from actorProxy.Handle: {error}\",\n                                getCurrentInstantExtended (),\n                                (graceError.ToString())\n                            )\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                log.LogDebug(\n                    \"{CurrentInstant}: In Owner.Server.processCommand: validationsPassed: {validationsPassed}.\",\n                    getCurrentInstantExtended (),\n                    validationsPassed\n                )\n\n                if validationsPassed then\n                    let! cmd = command parameters\n\n                    let! result = handleCommand graceIds.OwnerIdString cmd\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Finished {path}; Status code: {statusCode}; OwnerId: {ownerId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        context.Request.Path,\n                        context.Response.StatusCode,\n                        graceIds.OwnerIdString\n                    )\n\n                    return result\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = OwnerError.getErrorMessage error\n                    log.LogDebug(\"{CurrentInstant}: error: {error}\", getCurrentInstantExtended (), errorMessage)\n\n                    let graceError =\n                        (GraceError.Create errorMessage (getCorrelationId context))\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(\"Command\", commandName)\n                            .enhance(\"Path\", context.Request.Path.Value)\n                            .enhance (\"Error\", errorMessage)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Owner.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    (getCorrelationId context)\n                )\n\n                let graceError =\n                    (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Generic processor for all Owner queries.\n    let processQuery<'T, 'U when 'T :> OwnerParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (maxCount: int)\n        (query: QueryResult<IOwnerActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    // Get the actor proxy for this owner.\n                    let actorProxy = Owner.CreateActorProxy graceIds.OwnerId correlationId\n\n                    // Execute the query.\n                    let! queryResult = query context maxCount actorProxy\n\n                    // Wrap the result in a GraceReturnValue.\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError =\n                        (GraceError.Create (OwnerError.getErrorMessage error) correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.Create $\"{ExceptionResponse.Create ex}\" correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Create an owner.\n    let Create: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: CreateOwnerParameters) =\n                    [|\n                        Owner.ownerIdDoesNotExist parameters.OwnerId parameters.CorrelationId OwnerIdAlreadyExists\n                        Owner.ownerNameDoesNotExist parameters.OwnerName parameters.CorrelationId OwnerNameAlreadyExists\n                    |]\n\n                let command (parameters: CreateOwnerParameters) =\n                    let ownerIdGuid = Guid.Parse(parameters.OwnerId)\n\n                    Create(ownerIdGuid, OwnerName parameters.OwnerName)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Create)\n                return! processCommand context validations command\n            }\n\n    /// Set the name of an owner.\n    let SetName: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOwnerNameParameters) =\n                    [|\n                        String.isNotEmpty parameters.NewName OwnerError.OwnerNameIsRequired\n                        String.isValidGraceName parameters.NewName OwnerError.InvalidOwnerName\n                        Owner.ownerIsNotDeleted context parameters.CorrelationId OwnerError.OwnerIsDeleted\n                        Owner.ownerNameDoesNotExist parameters.NewName parameters.CorrelationId OwnerError.OwnerNameAlreadyExists\n                    |]\n\n                let command (parameters: SetOwnerNameParameters) =\n                    SetName(OwnerName parameters.NewName)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetName)\n                return! processCommand context validations command\n            }\n\n    /// Set the owner type (Public, Private).\n    let SetType: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOwnerTypeParameters) =\n                    [|\n                        String.isNotEmpty parameters.OwnerType OwnerError.OwnerTypeIsRequired\n                        DiscriminatedUnion.isMemberOf<OwnerType, OwnerError> parameters.OwnerType OwnerError.InvalidOwnerType\n                        Owner.ownerIsNotDeleted context parameters.CorrelationId OwnerError.OwnerIsDeleted\n                    |]\n\n                let command (parameters: SetOwnerTypeParameters) =\n                    OwnerCommand.SetType(\n                        discriminatedUnionFromString<OwnerType>(\n                            parameters.OwnerType\n                        )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetType)\n                return! processCommand context validations command\n            }\n\n    /// Set the owner search visibility (Visible, NotVisible).\n    let SetSearchVisibility: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOwnerSearchVisibilityParameters) =\n                    [|\n                        String.isNotEmpty parameters.SearchVisibility OwnerError.SearchVisibilityIsRequired\n                        DiscriminatedUnion.isMemberOf<SearchVisibility, OwnerError> parameters.SearchVisibility OwnerError.InvalidSearchVisibility\n                        Owner.ownerIsNotDeleted context parameters.CorrelationId OwnerIsDeleted\n                    |]\n\n                let command (parameters: SetOwnerSearchVisibilityParameters) =\n                    OwnerCommand.SetSearchVisibility(\n                        Utilities\n                            .discriminatedUnionFromString<SearchVisibility>(\n                                parameters.SearchVisibility\n                            )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetSearchVisibility)\n                return! processCommand context validations command\n            }\n\n    /// Set the owner's description.\n    let SetDescription: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetOwnerDescriptionParameters) =\n                    [|\n                        String.isNotEmpty parameters.Description OwnerError.DescriptionIsRequired\n                        String.maxLength parameters.Description 2048 OwnerError.DescriptionIsTooLong\n                        Owner.ownerIsNotDeleted context parameters.CorrelationId OwnerError.OwnerIsDeleted\n                    |]\n\n                let command (parameters: SetOwnerDescriptionParameters) =\n                    OwnerCommand.SetDescription(parameters.Description)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetDescription)\n                return! processCommand context validations command\n            }\n\n    /// List the organizations for an owner.\n    let ListOrganizations: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                try\n                    let validations (parameters: ListOrganizationsParameters) =\n                        [|\n                            Guid.isValidAndNotEmptyGuid parameters.OwnerId OwnerError.InvalidOwnerId\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IOwnerActor) =\n                        task {\n                            let! organizations = actorProxy.ListOrganizations(getCorrelationId context)\n                            return! context.WriteJsonAsync(organizations)\n                        }\n\n                    let! parameters = context |> parse<ListOrganizationsParameters>\n                    return! processQuery context parameters validations 1 query\n                with\n                | ex ->\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Delete an owner.\n    let Delete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: DeleteOwnerParameters) =\n                    [|\n                        String.isNotEmpty parameters.DeleteReason OwnerError.DeleteReasonIsRequired\n                        Owner.ownerIsNotDeleted context parameters.CorrelationId OwnerError.OwnerIsDeleted\n                    |]\n\n                let command (parameters: DeleteOwnerParameters) =\n                    OwnerCommand.DeleteLogical(parameters.Force, parameters.DeleteReason)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Delete)\n                return! processCommand context validations command\n            }\n\n    /// Undelete a previously-deleted owner.\n    let Undelete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: OwnerParameters) =\n                    [|\n                        Owner.ownerIsDeleted context parameters.CorrelationId OwnerError.OwnerIsNotDeleted\n                    |]\n\n                let command (parameters: OwnerParameters) = OwnerCommand.Undelete |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Undelete)\n                return! processCommand context validations command\n            }\n\n    /// Get an owner.\n    let Get: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetOwnerParameters) =\n                        [|\n                            Owner.ownerIsNotDeleted context parameters.CorrelationId OwnerError.OwnerIsDeleted\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IOwnerActor) = actorProxy.Get(getCorrelationId context)\n\n                    let! parameters = context |> parse<GetOwnerParameters>\n\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; OwnerId: {ownerId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.OwnerIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; OwnerId: {ownerId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.OwnerIdString\n                    )\n\n                    let graceError =\n                        (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result500ServerError graceError\n            }\n"
  },
  {
    "path": "src/Grace.Server/PartitionKeyProvider.fs",
    "content": "namespace Grace.Server\n\nopen Orleans.Providers.CosmosDB\nopen System.Threading.Tasks\n\n/// Provides a custom partition key for Orleans grains stored in CosmosDB.\ntype PartitionKeyProvider() =\n    interface IPartitionKeyProvider with\n        member this.GetPartitionKey(grainType: string, grainId: GrainId) : ValueTask<string> =\n            // Use the sanitized string version of the RepositoryId as the partition key.\n            ValueTask<string>(CosmosIdSanitizer.Sanitize(grainId.ToString()))\n"
  },
  {
    "path": "src/Grace.Server/Policy.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Policy\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule Policy =\n    type Validations<'T when 'T :> PolicyParameters> = 'T -> ValueTask<Result<unit, PolicyError>> array\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Policy.Server\")\n\n    let activitySource = new ActivitySource(\"Policy\")\n\n    let processCommand<'T when 'T :> PolicyParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<PolicyCommand>) =\n        task {\n            let commandName = context.Items[\"Command\"] :?> string\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let handleCommand targetBranchId cmd =\n                    task {\n                        let actorProxy = Policy.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n                        let metadata = createMetadata context\n\n                        match! actorProxy.Handle cmd metadata with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, targetBranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, targetBranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let targetBranchId = Guid.Parse(parameters.TargetBranchId)\n                    return! handleCommand targetBranchId cmd\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = PolicyError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Policy.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> PolicyParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (query: QueryResult<IPolicyActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let targetBranchId = Guid.Parse(parameters.TargetBranchId)\n                    let actorProxy = Policy.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n                    let! queryResult = query context 0 actorProxy\n\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof BranchId, targetBranchId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = PolicyError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let internal validateAcknowledgeParameters (parameters: AcknowledgePolicyParameters) =\n        [|\n            Guid.isValidAndNotEmptyGuid parameters.TargetBranchId PolicyError.InvalidTargetBranchId\n            String.isNotEmpty parameters.PolicySnapshotId PolicyError.InvalidPolicySnapshotId\n        |]\n\n    /// Gets the current policy snapshot.\n    let GetCurrent: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: GetPolicyParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId PolicyError.InvalidTargetBranchId\n                    |]\n\n                let query (context: HttpContext) _ (actorProxy: IPolicyActor) = actorProxy.GetCurrent(getCorrelationId context)\n\n                let! parameters = context |> parse<GetPolicyParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                context.Items[ \"Command\" ] <- \"GetCurrent\"\n                return! processQuery context parameters validations query\n            }\n\n    /// Acknowledges a policy snapshot.\n    let Acknowledge: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: AcknowledgePolicyParameters) = validateAcknowledgeParameters parameters\n\n                let command (parameters: AcknowledgePolicyParameters) =\n                    let policySnapshotId = PolicySnapshotId parameters.PolicySnapshotId\n                    let note = if String.IsNullOrEmpty(parameters.Note) then None else Some parameters.Note\n\n                    let principal =\n                        if\n                            isNull context.User\n                            || isNull context.User.Identity\n                            || String.IsNullOrEmpty(context.User.Identity.Name)\n                        then\n                            Constants.GraceSystemUser\n                        else\n                            context.User.Identity.Name\n\n                    PolicyCommand.Acknowledge(policySnapshotId, UserId principal, note)\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof Acknowledge\n                return! processCommand context validations command\n            }\n"
  },
  {
    "path": "src/Grace.Server/Program.Server.fs",
    "content": "namespace Grace.Server\n\nopen Azure.Core\nopen Azure.Data.Tables\nopen Azure.Identity\nopen Azure.Storage.Blobs\nopen dotenv.net\nopen Grace.Actors.Interfaces\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.AzureEnvironment\nopen Grace.Shared.Constants\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen MessagePack\nopen Microsoft.AspNetCore\nopen Microsoft.AspNetCore.Hosting\nopen Microsoft.AspNetCore.Server.Kestrel.Core\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Extensions.Configuration\nopen Microsoft.Extensions.Hosting\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.Caching.Memory\nopen Orleans\nopen Orleans.Clustering.AzureStorage\nopen Orleans.Hosting\nopen Orleans.Configuration\nopen Orleans.Persistence\nopen Orleans.Persistence.Cosmos\nopen Orleans.Runtime\nopen Orleans.Hosting\nopen Orleans.Serialization\nopen Orleans.Storage\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.IO\nopen System.Linq\nopen System.Net.Http\nopen System.Net.Security\nopen System.Security.Authentication\nopen System.Text.Json\nopen System.Threading\nopen System.Threading.Tasks\nopen Grace.Actors\nopen System.Runtime.CompilerServices\nopen Microsoft.AspNetCore.Builder\nopen System.Diagnostics\nopen System.Globalization\nopen Azure.Storage\n\nmodule OrleansFsharpFix =\n    // Grace.Orleans.CodeGen is the name of the C# codegen project.\n    [<assembly: Orleans.ApplicationPartAttribute(\"Grace.Orleans.CodeGen\")>]\n    do ()\n\nmodule Program =\n\n    [<InternalsVisibleTo(\"Host\")>]\n    do ()\n\n    type FileLogger(name: string, minLevel: LogLevel, writer: StreamWriter, scopeProvider: unit -> IExternalScopeProvider) =\n        let emptyScope =\n            { new IDisposable with\n                member _.Dispose() = ()\n            }\n\n        interface ILogger with\n            member _.IsEnabled level = level >= minLevel\n\n            member _.BeginScope<'TState>(state: 'TState) : IDisposable =\n                match scopeProvider () with\n                | null -> emptyScope\n                | provider -> provider.Push(state)\n\n            member _.Log<'TState>(level: LogLevel, eventId: EventId, state: 'TState, ex: exn, formatter: Func<'TState, exn, string>) =\n                if level >= minLevel && not (isNull formatter) then\n                    let message = formatter.Invoke(state, ex)\n\n                    let scopes =\n                        match scopeProvider () with\n                        | null -> String.Empty\n                        | provider ->\n                            let items = ResizeArray<string>()\n                            provider.ForEachScope((fun scope _ -> items.Add(scope.ToString())), ())\n\n                            if items.Count = 0 then\n                                String.Empty\n                            else\n                                let connector = \" => \"\n                                $\" [Scope: {String.Join(connector, items)}]\"\n\n                    let exceptionText = if isNull ex then String.Empty else Environment.NewLine + ex.ToString()\n\n                    lock writer (fun () ->\n                        writer.WriteLine($\"{DateTime.UtcNow:O} [{level}] {name}: {message}{scopes}{exceptionText}\")\n                        writer.Flush())\n\n    type FileLoggerProvider(filePath: string, minLevel: LogLevel) =\n        let fileStream =\n            try\n                new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read)\n            with\n            | :? IOException -> raise (InvalidOperationException($\"Log file '{filePath}' already exists; refusing to overwrite.\"))\n\n        let writer = new StreamWriter(fileStream)\n        let mutable scopeProvider: IExternalScopeProvider = new LoggerExternalScopeProvider()\n\n        do writer.AutoFlush <- true\n\n        interface ILoggerProvider with\n            member _.CreateLogger(categoryName) = new FileLogger(categoryName, minLevel, writer, (fun () -> scopeProvider)) :> ILogger\n            member _.Dispose() = writer.Dispose()\n\n        interface ISupportExternalScope with\n            member _.SetScopeProvider(provider) =\n                scopeProvider <-\n                    match provider with\n                    | null -> new LoggerExternalScopeProvider() :> IExternalScopeProvider\n                    | _ -> provider\n\n    type SystemTextJsonGrainStorageSerializer(options: JsonSerializerOptions) =\n        interface IGrainStorageSerializer with\n            member _.Serialize(obj) =\n                let t = obj.GetType()\n                let bytes = JsonSerializer.SerializeToUtf8Bytes(obj, t, options)\n                BinaryData(bytes)\n\n            member _.Deserialize<'T>(data: BinaryData) =\n                use stream = data.ToStream()\n                JsonSerializer.Deserialize<'T>(stream, options)\n\n    let private defaultAzureCredential = lazy (DefaultAzureCredential())\n\n    // Load environment variables from .env file, if it exists.\n    let envPaths =\n        [|\n            Path.Combine(AppContext.BaseDirectory, \"..\", \"..\", \"..\", \".env\") // during debug\n            Path.Combine(AppContext.BaseDirectory, \"..\", \"..\", \"..\", \"..\", \".env\")\n        |] // during debug\n\n    for envPath in envPaths do\n        let path = Path.GetFullPath(envPath)\n        logToConsole $\"Checking for .env file at {path}.\"\n\n        if File.Exists(path) then\n            logToConsole $\"Loading environment variables from {path}.\"\n            DotEnv.Load(DotEnvOptions(envFilePaths = [| path |], ignoreExceptions = true))\n\n    /// Configures and builds the generic host for the Grace server application.\n    let createHostBuilder (args: string []) (configuration: IConfiguration) =\n        let storageEndpoints = AzureEnvironment.storageEndpoints\n\n        let azureStorageConnectionString =\n            match storageEndpoints.ConnectionString with\n            | Some value -> value\n            | None -> Environment.GetEnvironmentVariable EnvironmentVariables.AzureStorageConnectionString\n\n        let azureCosmosDBConnectionString =\n            match AzureEnvironment.cosmosConnectionString with\n            | Some value -> value\n            | None -> Environment.GetEnvironmentVariable EnvironmentVariables.AzureCosmosDBConnectionString\n\n        let hasAzureStorageConnectionString =\n            not\n            <| String.IsNullOrWhiteSpace azureStorageConnectionString\n\n        let debugEnvironment = configuration[getConfigKey EnvironmentVariables.DebugEnvironment]\n        let isLocalDebug = String.Equals(debugEnvironment, \"Local\", StringComparison.OrdinalIgnoreCase)\n        let isAzureDebug = String.Equals(debugEnvironment, \"Azure\", StringComparison.OrdinalIgnoreCase)\n\n        let orleansClusterId = configuration[getConfigKey EnvironmentVariables.OrleansClusterId]\n        let orleansServiceId = configuration[getConfigKey EnvironmentVariables.OrleansServiceId]\n\n        let createTableClientOptions () =\n            let options = TableClientOptions()\n\n            if isLocalDebug || isAzureDebug then\n                options.Retry.Mode <- RetryMode.Fixed\n                options.Retry.Delay <- TimeSpan.FromMilliseconds(200.0)\n                options.Retry.MaxDelay <- TimeSpan.FromSeconds(1.0)\n                options.Retry.MaxRetries <- 1\n                options.Retry.NetworkTimeout <- TimeSpan.FromSeconds(5.0)\n\n            options\n\n        let waitForAzureTableReady (client: TableServiceClient) =\n            match AzureEnvironment.debugEnvironment with\n            | Some value when value.Equals(\"Local\", StringComparison.OrdinalIgnoreCase) ->\n                let sw = Stopwatch.StartNew()\n                let timeout = TimeSpan.FromSeconds(60.0)\n                let delay = TimeSpan.FromSeconds(1.0)\n\n                let rec poll attempt =\n                    try\n                        client.GetProperties() |> ignore\n                        logToConsole $\"Azure Table endpoint ready after {attempt} attempt(s).\"\n                    with\n                    | ex ->\n                        if sw.Elapsed >= timeout then\n                            logToConsole $\"Azure Table endpoint was not ready after {timeout.TotalSeconds} seconds: {ex.Message}\"\n                            reraise ()\n                        else\n                            Thread.Sleep(delay)\n                            poll (attempt + 1)\n\n                poll 0\n            | _ -> ()\n\n        let logDirectory =\n            let directoryValue = Environment.GetEnvironmentVariable EnvironmentVariables.GraceLogDirectory\n\n            if String.IsNullOrWhiteSpace directoryValue then\n                invalidOp $\"Environment variable '{EnvironmentVariables.GraceLogDirectory}' must be set to a writable directory for log files.\"\n\n            let fullPath = Path.GetFullPath(directoryValue)\n            Directory.CreateDirectory(fullPath) |> ignore\n            fullPath\n\n        let logFileName =\n            getCurrentInstant()\n                .ToString(\"yyyy-MM-dd-HH-mm-ss\", CultureInfo.InvariantCulture)\n            + \".log\"\n\n        let logFilePath = Path.Combine(logDirectory, logFileName)\n        let fileLoggerProvider = new FileLoggerProvider(logFilePath, LogLevel.Debug)\n\n        logToConsole $\"Grace Server logs will be written to {logFilePath}\"\n\n        let hostBuilder = Host.CreateDefaultBuilder(args)\n\n        hostBuilder\n            .UseContentRoot(Directory.GetCurrentDirectory())\n            .UseOrleans(fun siloBuilder ->\n                siloBuilder\n                    .Configure<ClusterMembershipOptions>(fun (options: ClusterMembershipOptions) ->\n                        options.DefunctSiloExpiration <- TimeSpan.FromMinutes(5.0)\n                        options.DefunctSiloCleanupPeriod <- TimeSpan.FromMinutes(1.0)\n\n                        if isLocalDebug || isAzureDebug then\n                            options.IAmAliveTablePublishTimeout <- TimeSpan.FromSeconds(5.0)\n                            options.NumMissedTableIAmAliveLimit <- 1\n                            options.TableRefreshTimeout <- TimeSpan.FromSeconds(5.0)\n                            options.DeathVoteExpirationTimeout <- TimeSpan.FromSeconds(15.0)\n                            options.DefunctSiloExpiration <- TimeSpan.FromSeconds(15.0)\n                            options.DefunctSiloCleanupPeriod <- TimeSpan.FromSeconds(5.0))\n                    .Configure<ClusterOptions>(fun (options: ClusterOptions) ->\n                        options.ClusterId <- orleansClusterId\n                        options.ServiceId <- orleansServiceId)\n                    .Configure<SiloOptions>(fun (options: SiloOptions) -> options.SiloName <- $\"Silo-{orleansServiceId}\")\n                    .Configure<SiloMessagingOptions>(fun (options: SiloMessagingOptions) -> options.ResponseTimeout <- TimeSpan.FromSeconds(60.0))\n                    .Configure<ClientMessagingOptions>(fun (options: ClientMessagingOptions) -> options.ResponseTimeout <- TimeSpan.FromSeconds(60.0))\n                    .Configure<GrainCollectionOptions>(fun (options: GrainCollectionOptions) ->\n                        options.CollectionAge <- TimeSpan.FromMinutes(15.0)\n\n                        options.ClassSpecificCollectionAge[\n                            $\"{(typeof<GrainRepository.GrainRepositoryActor>)\n                                   .FullName}\"\n                        ] <- TimeSpan.FromMinutes(5.0))\n                    .UseAzureStorageClustering(fun (options: AzureStorageClusteringOptions) ->\n                        logToConsole\n                            $\"Orleans clustering using Azure Tables at {storageEndpoints.TableEndpoint}; account {storageEndpoints.AccountName}; debug env {debugEnvironment}; storage connection string present {hasAzureStorageConnectionString}; managed identity {AzureEnvironment.useManagedIdentity}; managed identity for storage {AzureEnvironment.useManagedIdentityForStorage}.\"\n\n                        let tableClientOptions = createTableClientOptions ()\n\n                        let tableServiceClient =\n                            if AzureEnvironment.useManagedIdentityForStorage then\n                                TableServiceClient(storageEndpoints.TableEndpoint, defaultAzureCredential.Value, tableClientOptions)\n                            else if String.IsNullOrWhiteSpace azureStorageConnectionString then\n                                invalidOp \"Azure Storage connection string must be configured for clustering when managed identity is disabled.\"\n                            else\n                                TableServiceClient(azureStorageConnectionString, tableClientOptions)\n\n                        waitForAzureTableReady tableServiceClient\n\n                        options.TableServiceClient <- tableServiceClient)\n                    .AddCosmosGrainStorage(\n                        GraceActorStorage,\n                        (fun (options: CosmosGrainStorageOptions) ->\n                            options.ContainerName <- configuration[getConfigKey EnvironmentVariables.AzureCosmosDBContainerName]\n                            options.DatabaseName <- configuration[getConfigKey EnvironmentVariables.AzureCosmosDBDatabaseName]\n\n                            logToConsole $\"Configuring Cosmos DB grain storage with database '{options.DatabaseName}' and container '{options.ContainerName}'.\"\n\n                            // All Cosmos DB resources should be created prior to starting Grace.\n                            options.IsResourceCreationEnabled <- false\n\n                            options.ConfigureCosmosClient (fun (serviceProvider: IServiceProvider) ->\n                                let cosmosClientOptions = CosmosClientOptions()\n                                cosmosClientOptions.ApplicationName <- \"Grace.Server\"\n                                cosmosClientOptions.LimitToEndpoint <- false\n                                cosmosClientOptions.UseSystemTextJsonSerializerWithOptions <- Grace.Shared.Constants.JsonSerializerOptions\n\n                                // If we're doing local debugging, and not using managed identity, we assume we're using the Cosmos DB emulator.\n                                // The emulator uses a self-signed certificate, so we need to bypass certificate validation.\n\n                                if isLocalDebug\n                                   && not\n                                      <| AzureEnvironment.useManagedIdentityForCosmos then\n                                    cosmosClientOptions.LimitToEndpoint <- true\n                                    cosmosClientOptions.ConnectionMode <- ConnectionMode.Gateway\n                                    cosmosClientOptions.EnableContentResponseOnWrite <- true\n\n                                    cosmosClientOptions.HttpClientFactory <-\n                                        fun () ->\n                                            logToConsole \"Creating custom HttpClient for Cosmos DB.\"\n\n                                            let handler =\n                                                new SocketsHttpHandler(\n                                                    SslOptions =\n                                                        new SslClientAuthenticationOptions(\n                                                            TargetHost = \"localhost\",\n                                                            RemoteCertificateValidationCallback = (fun _ _ _ _ -> true)\n                                                        )\n                                                )\n\n                                            new HttpClient(handler, disposeHandler = true)\n\n                                let cosmosClient =\n                                    if AzureEnvironment.useManagedIdentity then\n                                        let endpoint =\n                                            AzureEnvironment.tryGetCosmosEndpointUri ()\n                                            |> Option.defaultWith (fun () ->\n                                                invalidOp \"Azure Cosmos DB endpoint must be configured when using a managed identity.\")\n\n                                        new CosmosClient(endpoint.AbsoluteUri, defaultAzureCredential.Value, cosmosClientOptions)\n                                    else\n                                        if String.IsNullOrWhiteSpace azureCosmosDBConnectionString then\n                                            invalidOp \"Cosmos DB connection string must be configured when managed identity is disabled.\"\n\n                                        new CosmosClient(azureCosmosDBConnectionString, cosmosClientOptions)\n\n                                ValueTask.FromResult(cosmosClient))),\n                        typeof<GracePartitionKeyProvider>\n                    )\n                    .AddAzureBlobGrainStorage(\n                        GraceDiffStorage,\n                        (fun (options: AzureBlobStorageOptions) ->\n                            options.BlobServiceClient <- Context.blobServiceClient\n                            options.ContainerName <- configuration[getConfigKey EnvironmentVariables.DiffContainerName]\n                            options.GrainStorageSerializer <- SystemTextJsonGrainStorageSerializer(Constants.JsonSerializerOptions))\n                    )\n                    .AddActivityPropagation()\n\n                |> ignore\n\n                siloBuilder.AddMemoryGrainStorage(GraceInMemoryStorage)\n                |> ignore\n\n                siloBuilder.Services.AddSerializer (fun serializerBuilder ->\n                    serializerBuilder.AddJsonSerializer(\n                        isSupported =\n                            (fun _type ->\n                                not <| String.IsNullOrEmpty(_type.Namespace)\n                                && _type.Namespace.StartsWith(\"Grace\", StringComparison.InvariantCulture)),\n                        jsonSerializerOptions = Constants.JsonSerializerOptions\n                    )\n                    |> ignore)\n                |> ignore)\n            .ConfigureLogging(fun logConfig ->\n                logConfig\n                    .SetMinimumLevel(LogLevel.Debug)\n                    .AddFilter(\"Orleans\", LogLevel.Information)\n                    .AddFilter(\"Orleans.Providers\", LogLevel.Debug)\n                |> ignore\n\n                logConfig.AddProvider(fileLoggerProvider)\n                |> ignore\n\n                logConfig.AddOpenTelemetry(fun openTelemetryOptions -> openTelemetryOptions.IncludeScopes <- true)\n                |> ignore)\n            .ConfigureWebHostDefaults(fun webBuilder ->\n                webBuilder\n                    .UseStartup<Application.Startup>()\n                    .UseKestrel(fun kestrelServerOptions ->\n                        kestrelServerOptions.ConfigureEndpointDefaults(fun listenOptions -> listenOptions.Protocols <- HttpProtocols.Http1AndHttp2)\n\n                        kestrelServerOptions.ConfigureHttpsDefaults (fun options ->\n                            options.SslProtocols <- SslProtocols.Tls12 ||| SslProtocols.Tls13\n#if DEBUG\n                            options.AllowAnyClientCertificate()\n#endif\n                        ))\n                |> ignore)\n            .Build()\n\n    [<EntryPoint>]\n    let main args =\n        (task {\n            try\n                logToConsole \"----------------------------- Starting Grace Server ------------------------------\"\n\n                // Build the configuration\n                let environment = Environment.GetEnvironmentVariable(\"ASPNETCORE_ENVIRONMENT\")\n\n                let configurationBuilder =\n                    ConfigurationBuilder()\n                        .AddJsonFile(\"appsettings.json\", true, true) // Load appsettings.json\n\n                if not <| String.IsNullOrWhiteSpace(environment) then\n                    configurationBuilder.AddJsonFile($\"appsettings.{environment}.json\", true, true) // Load environment-specific settings\n                    |> ignore\n\n                let configuration =\n                    configurationBuilder\n                        .AddEnvironmentVariables()\n                        .AddUserSecrets() // Use `dotnet user-secrets` to store sensitive settings during development\n                        .Build()\n\n                // Store the configuration in memory cache for easy access throughout the application.\n                use configurationEntry = memoryCache.CreateEntry(MemoryCache.GraceConfiguration)\n                configurationEntry.Value <- configuration\n                configurationEntry.Priority <- CacheItemPriority.NeverRemove\n                configurationEntry.Dispose()\n\n                logToConsole \"Configuration settings saved in memory cache.\"\n\n                use host = createHostBuilder args configuration\n\n                // Placing some much-used services into ApplicationContext where they're easy to find.\n                Context.setHostServiceProvider host.Services\n\n                let orleansClient = host.Services.GetService(typeof<IGrainFactory>) :?> IGrainFactory\n                ApplicationContext.setOrleansClient orleansClient\n\n                let loggerFactory = host.Services.GetService(typeof<ILoggerFactory>) :?> ILoggerFactory\n                ApplicationContext.setLoggerFactory loggerFactory\n\n                // Dump out configuration settings at startup for debugging purposes.\n                //logToConsole \"Configuration settings:\"\n                //for pair in config.AsEnumerable() do\n                //    logToConsole $\"  {pair.Key} = {pair.Value}\"\n\n                do! host.RunAsync()\n\n                return 0 // Return an integer exit code\n            with\n            | ex ->\n                logToConsole $\"Fatal error starting Grace Server.{Environment.NewLine}{ex.ToStringDemystified()}\"\n                return -1\n        })\n            .Result\n"
  },
  {
    "path": "src/Grace.Server/PromotionSet.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.PromotionSet\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.PromotionSet\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen System\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule PromotionSet =\n    type Validations<'T when 'T :> PromotionSetParameters> = 'T -> ValueTask<Result<unit, QueueError>> array\n\n    let activitySource = new ActivitySource(\"PromotionSet\")\n\n    let private parsePromotionSetIdOrNew (rawPromotionSetId: string) =\n        if String.IsNullOrWhiteSpace(rawPromotionSetId) then\n            Ok(Guid.NewGuid())\n        else\n            let mutable parsed = Guid.Empty\n\n            if\n                Guid.TryParse(rawPromotionSetId, &parsed)\n                && parsed <> Guid.Empty\n            then\n                Ok parsed\n            else\n                Error QueueError.InvalidPromotionSetId\n\n    let private validatePromotionPointers (promotionPointers: PromotionPointer list) =\n        if promotionPointers.IsEmpty then\n            Some \"At least one promotion pointer is required.\"\n        else\n            promotionPointers\n            |> List.tryPick (fun pointer ->\n                if pointer.BranchId = BranchId.Empty then\n                    Some \"PromotionPointer.BranchId must be a non-empty Guid.\"\n                elif pointer.ReferenceId = ReferenceId.Empty then\n                    Some \"PromotionPointer.ReferenceId must be a non-empty Guid.\"\n                elif pointer.DirectoryVersionId = DirectoryVersionId.Empty then\n                    Some \"PromotionPointer.DirectoryVersionId must be a non-empty Guid.\"\n                else\n                    Option.None)\n\n    let private processCommand<'T when 'T :> PromotionSetParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (promotionSetId: PromotionSetId)\n        (command: PromotionSetCommand)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let metadata = createMetadata context\n            let parameterDictionary = getParametersAsDictionary parameters\n            let validationResults = validations parameters\n            let! validationsPassed = validationResults |> allPass\n\n            if validationsPassed then\n                let actorProxy = PromotionSet.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n\n                match! actorProxy.Handle command metadata with\n                | Ok graceReturnValue ->\n                    graceReturnValue\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof PromotionSetId, promotionSetId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    return! context |> result200Ok graceReturnValue\n                | Error graceError ->\n                    graceError\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof PromotionSetId, promotionSetId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    return! context |> result400BadRequest graceError\n            else\n                let! validationError = validationResults |> getFirstError\n                let graceError = GraceError.Create (QueueError.getErrorMessage validationError) correlationId\n                return! context |> result400BadRequest graceError\n        }\n\n    let private processGet (context: HttpContext) (parameters: GetPromotionSetParameters) (promotionSetId: PromotionSetId) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = PromotionSet.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n            let! promotionSet = actorProxy.Get correlationId\n\n            let graceReturnValue =\n                (GraceReturnValue.Create promotionSet correlationId)\n                    .enhance(getParametersAsDictionary parameters)\n                    .enhance(nameof OwnerId, graceIds.OwnerId)\n                    .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                    .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                    .enhance(nameof PromotionSetId, promotionSetId)\n                    .enhance (\"Path\", context.Request.Path.Value)\n\n            return! context |> result200Ok graceReturnValue\n        }\n\n    let private processGetEvents (context: HttpContext) (parameters: GetPromotionSetEventsParameters) (promotionSetId: PromotionSetId) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = PromotionSet.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n            let! events = actorProxy.GetEvents correlationId\n\n            let graceReturnValue =\n                (GraceReturnValue.Create events correlationId)\n                    .enhance(getParametersAsDictionary parameters)\n                    .enhance(nameof OwnerId, graceIds.OwnerId)\n                    .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                    .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                    .enhance(nameof PromotionSetId, promotionSetId)\n                    .enhance (\"Path\", context.Request.Path.Value)\n\n            return! context |> result200Ok graceReturnValue\n        }\n\n    /// Creates a promotion set.\n    let Create: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let! parameters = context |> parse<CreatePromotionSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                match parsePromotionSetIdOrNew parameters.PromotionSetId with\n                | Error validationError ->\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create (QueueError.getErrorMessage validationError) (getCorrelationId context))\n                | Ok promotionSetId ->\n                    let validations (_: CreatePromotionSetParameters) =\n                        [|\n                            Guid.isValidAndNotEmptyGuid parameters.TargetBranchId QueueError.InvalidTargetBranchId\n                        |]\n\n                    let command =\n                        PromotionSetCommand.CreatePromotionSet(\n                            promotionSetId,\n                            graceIds.OwnerId,\n                            graceIds.OrganizationId,\n                            graceIds.RepositoryId,\n                            Guid.Parse(parameters.TargetBranchId)\n                        )\n\n                    return! processCommand context parameters validations promotionSetId command\n            }\n\n    /// Gets a promotion set.\n    let Get: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<GetPromotionSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: GetPromotionSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                    return! processGet context parameters promotionSetId\n                else\n                    let! validationError = validationResults |> getFirstError\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create (QueueError.getErrorMessage validationError) correlationId)\n            }\n\n    /// Gets all promotion set events.\n    let GetEvents: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<GetPromotionSetEventsParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: GetPromotionSetEventsParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                    return! processGetEvents context parameters promotionSetId\n                else\n                    let! validationError = validationResults |> getFirstError\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create (QueueError.getErrorMessage validationError) correlationId)\n            }\n\n    /// Updates the input promotion pointers for a promotion set.\n    let UpdateInputPromotions: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<UpdatePromotionSetInputPromotionsParameters>\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: UpdatePromotionSetInputPromotionsParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                match validatePromotionPointers parameters.PromotionPointers with\n                | Option.Some errorMessage ->\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create errorMessage correlationId)\n                | Option.None ->\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                    let command = PromotionSetCommand.UpdateInputPromotions parameters.PromotionPointers\n                    return! processCommand context parameters validations promotionSetId command\n            }\n\n    /// Requests server-side recomputation for a promotion set.\n    let Recompute: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let! parameters = context |> parse<RecomputePromotionSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: RecomputePromotionSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n\n                let reason =\n                    if String.IsNullOrWhiteSpace(parameters.Reason) then\n                        Option.None\n                    else\n                        Option.Some parameters.Reason\n\n                let command = PromotionSetCommand.RecomputeStepsIfStale reason\n                return! processCommand context parameters validations promotionSetId command\n            }\n\n    /// Applies a promotion set.\n    let Apply: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let! parameters = context |> parse<ApplyPromotionSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: ApplyPromotionSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                let command = PromotionSetCommand.Apply\n                return! processCommand context parameters validations promotionSetId command\n            }\n\n    /// Resolves blocked conflicts for a promotion set.\n    let ResolveConflicts (routePromotionSetId: PromotionSetId) : HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<ResolvePromotionSetConflictsParameters>\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                parameters.PromotionSetId <- $\"{routePromotionSetId}\"\n\n                let validations (_: ResolvePromotionSetConflictsParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                if parameters.StepsComputationAttempt <= 0 then\n                    return!\n                        context\n                        |> result400BadRequest (\n                            GraceError.Create (ValidationResultError.getErrorMessage ValidationResultError.InvalidStepsComputationAttempt) correlationId\n                        )\n                elif parameters.Decisions.IsEmpty then\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create \"At least one conflict resolution decision is required.\" correlationId)\n                else\n                    let mutable stepId = Guid.Empty\n\n                    if not (Guid.TryParse(parameters.StepId, &stepId) && stepId <> Guid.Empty) then\n                        return!\n                            context\n                            |> result400BadRequest (\n                                GraceError.Create (ValidationResultError.getErrorMessage ValidationResultError.InvalidPromotionSetStepId) correlationId\n                            )\n                    else\n                        let promotionSetId = routePromotionSetId\n                        let actorProxy = PromotionSet.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n                        let! currentPromotionSet = actorProxy.Get correlationId\n\n                        if currentPromotionSet.Status\n                           <> PromotionSetStatus.Blocked then\n                            return!\n                                context\n                                |> result400BadRequest (GraceError.Create \"PromotionSet is not blocked for conflict resolution.\" correlationId)\n                        elif currentPromotionSet.StepsComputationAttempt\n                             <> parameters.StepsComputationAttempt then\n                            return!\n                                context\n                                |> result400BadRequest (GraceError.Create \"StepsComputationAttempt does not match current PromotionSet state.\" correlationId)\n                        else\n                            let command = PromotionSetCommand.ResolveConflicts(stepId, parameters.Decisions)\n                            return! processCommand context parameters validations promotionSetId command\n            }\n\n    /// Logically deletes a promotion set.\n    let Delete: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let! parameters = context |> parse<DeletePromotionSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: DeletePromotionSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                let command = PromotionSetCommand.DeleteLogical(parameters.Force, DeleteReason parameters.DeleteReason)\n                return! processCommand context parameters validations promotionSetId command\n            }\n"
  },
  {
    "path": "src/Grace.Server/Properties/PublishProfiles/DisableContainerBuild.pubxml",
    "content": "<Project>\n  <PropertyGroup>\n    <PublishContainer>false</PublishContainer>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.Server/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Grace.Server\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"dotnetRunMessages\": true\n    },\n    \"Docker\": {\n      \"commandName\": \"Docker\",\n      \"launchBrowser\": true,\n      \"launchUrl\": \"{Scheme}://{ServiceHost}:{ServicePort}\",\n      \"publishAllPorts\": true,\n      \"useSSL\": true\n    }\n  },\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:22992\",\n      \"sslPort\": 44336\n    }\n  }\n}"
  },
  {
    "path": "src/Grace.Server/Queue.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Queue\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.RequiredAction\nopen Grace.Types.Policy\nopen Grace.Types.Queue\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule Queue =\n    type Validations<'T when 'T :> QueueParameters> = 'T -> ValueTask<Result<unit, QueueError>> array\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Queue.Server\")\n\n    let activitySource = new ActivitySource(\"Queue\")\n\n    let internal requiresPolicySnapshotForInitialization (queueExists: bool) (policySnapshotId: string) =\n        not queueExists\n        && String.IsNullOrEmpty(policySnapshotId)\n\n    let processCommand<'T when 'T :> QueueParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<PromotionQueueCommand>) =\n        task {\n            let commandName = context.Items[\"Command\"] :?> string\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let handleCommand targetBranchId cmd =\n                    task {\n                        let actorProxy = PromotionQueue.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n                        let metadata = createMetadata context\n\n                        match! actorProxy.Handle cmd metadata with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, targetBranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, targetBranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let targetBranchId = Guid.Parse(parameters.TargetBranchId)\n                    return! handleCommand targetBranchId cmd\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = QueueError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Queue.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processCommandWithParameters<'T when 'T :> QueueParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (command: 'T -> ValueTask<PromotionQueueCommand>)\n        =\n        task {\n            let commandName = context.Items[\"Command\"] :?> string\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommandWithParameters\", ActivityKind.Server)\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let handleCommand targetBranchId cmd =\n                    task {\n                        let actorProxy = PromotionQueue.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n                        let metadata = createMetadata context\n\n                        match! actorProxy.Handle cmd metadata with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, targetBranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof BranchId, targetBranchId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let targetBranchId = Guid.Parse(parameters.TargetBranchId)\n                    return! handleCommand targetBranchId cmd\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = QueueError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Queue.Server.processCommandWithParameters. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> QueueParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (query: QueryResult<IPromotionQueueActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let targetBranchId = Guid.Parse(parameters.TargetBranchId)\n                    let actorProxy = PromotionQueue.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n                    let! queryResult = query context 0 actorProxy\n\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof BranchId, targetBranchId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = QueueError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Returns queue status for a target branch.\n    let Status: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: QueueStatusParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId QueueError.InvalidTargetBranchId\n                    |]\n\n                let query (context: HttpContext) _ (actorProxy: IPromotionQueueActor) = actorProxy.Get(getCorrelationId context)\n\n                let! parameters = context |> parse<QueueStatusParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                context.Items[ \"Command\" ] <- \"Status\"\n                return! processQuery context parameters validations query\n            }\n\n    /// Pauses a promotion queue.\n    let Pause: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: QueueActionParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId QueueError.InvalidTargetBranchId\n                    |]\n\n                let command (_: QueueActionParameters) = PromotionQueueCommand.Pause |> returnValueTask\n                context.Items[ \"Command\" ] <- nameof Pause\n                return! processCommand context validations command\n            }\n\n    /// Resumes a promotion queue.\n    let Resume: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: QueueActionParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId QueueError.InvalidTargetBranchId\n                    |]\n\n                let command (_: QueueActionParameters) = PromotionQueueCommand.Resume |> returnValueTask\n                context.Items[ \"Command\" ] <- nameof Resume\n                return! processCommand context validations command\n            }\n\n    /// Enqueues a promotion set in the promotion queue.\n    let Enqueue: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let validations (parameters: EnqueueParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId QueueError.InvalidTargetBranchId\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                        (if String.IsNullOrEmpty(parameters.PolicySnapshotId) then\n                             Ok() |> returnValueTask\n                         else\n                             String.isNotEmpty parameters.PolicySnapshotId QueueError.InvalidPolicySnapshotId)\n                    |]\n\n                let! parameters = context |> parse<EnqueueParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let targetBranchId = Guid.Parse(parameters.TargetBranchId)\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                    let actorProxy = PromotionQueue.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n                    let promotionSetActorProxy = PromotionSet.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n                    let metadata = createMetadata context\n\n                    let runEnqueue () =\n                        task {\n                            context.Items[ \"Command\" ] <- nameof Enqueue\n\n                            let command (_: EnqueueParameters) =\n                                PromotionQueueCommand.Enqueue promotionSetId\n                                |> returnValueTask\n\n                            return! processCommandWithParameters context parameters (fun _ -> [||]) command\n                        }\n\n                    let continueEnqueue () =\n                        task {\n                            let! exists = actorProxy.Exists correlationId\n\n                            if not exists then\n                                if requiresPolicySnapshotForInitialization exists parameters.PolicySnapshotId then\n                                    let graceError = GraceError.Create \"PolicySnapshotId is required to initialize the queue.\" correlationId\n\n                                    return! context |> result400BadRequest graceError\n                                else\n                                    let initializeCommand = PromotionQueueCommand.Initialize(targetBranchId, PolicySnapshotId parameters.PolicySnapshotId)\n\n                                    match! actorProxy.Handle initializeCommand metadata with\n                                    | Error error -> return! context |> result400BadRequest error\n                                    | Ok _ -> return! runEnqueue ()\n                            else\n                                return! runEnqueue ()\n                        }\n\n                    let! promotionSetExists = promotionSetActorProxy.Exists correlationId\n\n                    if not promotionSetExists then\n                        let graceError = GraceError.Create \"The specified promotion set does not exist.\" correlationId\n                        return! context |> result400BadRequest graceError\n                    else\n                        return! continueEnqueue ()\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = QueueError.getErrorMessage error\n                    let graceError = GraceError.Create errorMessage correlationId\n                    return! context |> result400BadRequest graceError\n            }\n\n    /// Dequeues a promotion set from the promotion queue.\n    let Dequeue: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: PromotionSetActionParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId QueueError.InvalidTargetBranchId\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId QueueError.InvalidPromotionSetId\n                    |]\n\n                let command (parameters: PromotionSetActionParameters) =\n                    PromotionQueueCommand.Dequeue(Guid.Parse(parameters.PromotionSetId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof Dequeue\n                return! processCommand context validations command\n            }\n"
  },
  {
    "path": "src/Grace.Server/Reminder.Server.fs",
    "content": "namespace Grace.Server\n\nopen FSharpPlus\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Reminder\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.AspNetCore.Mvc\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen NodaTime.Text\nopen OpenTelemetry.Trace\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Globalization\nopen System.Linq\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\n\nmodule Reminder =\n\n    type Validations<'T when 'T :> ReminderParameters> = 'T -> ValueTask<Result<unit, ReminderError>> array\n\n    let activitySource = new ActivitySource(\"Reminder\")\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Reminder.Server\")\n\n    let processQuery<'T, 'U when 'T :> ReminderParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (query: HttpContext -> 'T -> Task<'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! queryResult = query context parameters\n\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError =\n                        (GraceError.Create (ReminderError.getErrorMessage error.Value) correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Reminder.Server.processQuery; Path: {path}; CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    context.Request.Path,\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.Create $\"{ExceptionResponse.Create ex}\" correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processCommand<'T when 'T :> ReminderParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (command: HttpContext -> 'T -> Task<Result<string, string>>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! commandResult = command context parameters\n\n                    match commandResult with\n                    | Ok message ->\n                        let graceReturnValue =\n                            (GraceReturnValue.Create message correlationId)\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance (\"Path\", context.Request.Path.Value)\n\n                        return! context |> result200Ok graceReturnValue\n                    | Error errorMessage ->\n                        let graceError =\n                            (GraceError.Create errorMessage correlationId)\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance (\"Path\", context.Request.Path.Value)\n\n                        return! context |> result400BadRequest graceError\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError =\n                        (GraceError.Create (ReminderError.getErrorMessage error.Value) correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Reminder.Server.processCommand; Path: {path}; CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    context.Request.Path,\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.Create $\"{ExceptionResponse.Create ex}\" correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Lists reminders for a repository with optional filters.\n    let List: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: ListRemindersParameters) = [||]\n\n                    let query (context: HttpContext) (parameters: ListRemindersParameters) =\n                        task {\n                            let correlationId = getCorrelationId context\n                            let graceIds = getGraceIds context\n\n                            // Parse optional filters\n                            let reminderTypeFilter =\n                                if String.IsNullOrEmpty(parameters.ReminderType) then\n                                    None\n                                else\n                                    Some parameters.ReminderType\n\n                            let actorNameFilter =\n                                if String.IsNullOrEmpty(parameters.ActorName) then\n                                    None\n                                else\n                                    Some parameters.ActorName\n\n                            let dueAfter =\n                                if String.IsNullOrEmpty(parameters.DueAfter) then\n                                    None\n                                else\n                                    let parseResult = InstantPattern.ExtendedIso.Parse(parameters.DueAfter)\n\n                                    if parseResult.Success then Some parseResult.Value else None\n\n                            let dueBefore =\n                                if String.IsNullOrEmpty(parameters.DueBefore) then\n                                    None\n                                else\n                                    let parseResult = InstantPattern.ExtendedIso.Parse(parameters.DueBefore)\n\n                                    if parseResult.Success then Some parseResult.Value else None\n\n                            return! getReminders graceIds parameters.MaxCount reminderTypeFilter actorNameFilter dueAfter dueBefore correlationId\n                        }\n\n                    let! parameters = context |> parse<ListRemindersParameters>\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    let! result = processQuery context parameters validations query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Gets a specific reminder by its ID.\n    let Get: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReminderParameters) =\n                        [|\n                            String.isNotEmpty parameters.ReminderId ReminderIdIsRequired\n                        |]\n\n                    let query (context: HttpContext) (parameters: GetReminderParameters) =\n                        task {\n                            let correlationId = getCorrelationId context\n                            let reminderId = Guid.Parse(parameters.ReminderId)\n                            let! reminderOption = getReminderById reminderId correlationId\n\n                            return\n                                match reminderOption with\n                                | Some reminder -> reminder\n                                | None -> ReminderDto.Default\n                        }\n\n                    let! parameters = context |> parse<GetReminderParameters>\n                    let! result = processQuery context parameters validations query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; ReminderId: {reminderId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        parameters.ReminderId\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Deletes a reminder.\n    let Delete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: DeleteReminderParameters) =\n                        [|\n                            String.isNotEmpty parameters.ReminderId ReminderIdIsRequired\n                        |]\n\n                    let command (context: HttpContext) (parameters: DeleteReminderParameters) =\n                        task {\n                            let correlationId = getCorrelationId context\n                            let reminderId = Guid.Parse(parameters.ReminderId)\n                            let! result = deleteReminder reminderId correlationId\n\n                            return\n                                match result with\n                                | Ok () -> Ok \"Reminder deleted successfully.\"\n                                | Error msg -> Error msg\n                        }\n\n                    let! parameters = context |> parse<DeleteReminderParameters>\n                    let! result = processCommand context parameters validations command\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; ReminderId: {reminderId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        parameters.ReminderId\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Updates the fire time for a reminder.\n    let UpdateTime: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: UpdateReminderTimeParameters) =\n                        [|\n                            String.isNotEmpty parameters.ReminderId ReminderIdIsRequired\n                            String.isNotEmpty parameters.FireAt InvalidReminderTime\n                        |]\n\n                    let command (context: HttpContext) (parameters: UpdateReminderTimeParameters) =\n                        task {\n                            let correlationId = getCorrelationId context\n                            let reminderId = Guid.Parse(parameters.ReminderId)\n\n                            // Parse the new fire time\n                            let parseResult = InstantPattern.ExtendedIso.Parse(parameters.FireAt)\n\n                            if parseResult.Success then\n                                let newFireTime = parseResult.Value\n                                let! reminderOption = getReminderById reminderId correlationId\n\n                                match reminderOption with\n                                | Some existingReminder ->\n                                    // Delete the old reminder and create a new one with updated time\n                                    let! _ = deleteReminder reminderId correlationId\n\n                                    let newReminder = { existingReminder with ReminderTime = newFireTime; CorrelationId = correlationId }\n\n                                    do! createReminder newReminder\n                                    return Ok \"Reminder time updated successfully.\"\n                                | None -> return Error \"Reminder not found.\"\n                            else\n                                return Error $\"Invalid fire time format: {parameters.FireAt}. Use ISO8601 format.\"\n                        }\n\n                    let! parameters = context |> parse<UpdateReminderTimeParameters>\n                    let! result = processCommand context parameters validations command\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; ReminderId: {reminderId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        parameters.ReminderId\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Parses a duration string like \"+15m\", \"+1h\", \"+1d\" and returns a Duration.\n    let parseDuration (durationString: string) =\n        if String.IsNullOrEmpty(durationString) then\n            None\n        else\n            try\n                let normalized =\n                    if durationString.StartsWith(\"+\") then\n                        durationString.Substring(1)\n                    else\n                        durationString\n\n                let lastChar = normalized[normalized.Length - 1]\n                let numericPart = normalized.Substring(0, normalized.Length - 1)\n                let mutable value = 0.0\n\n                if Double.TryParse(numericPart, NumberStyles.Float, CultureInfo.InvariantCulture, &value) then\n                    match lastChar with\n                    | 's' -> Some(Duration.FromSeconds(value))\n                    | 'm' -> Some(Duration.FromMinutes(value))\n                    | 'h' -> Some(Duration.FromHours(value))\n                    | 'd' -> Some(Duration.FromDays(value))\n                    | _ -> None\n                else\n                    None\n            with\n            | _ -> None\n\n    /// Reschedules a reminder relative to now.\n    let Reschedule: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: RescheduleReminderParameters) =\n                        [|\n                            String.isNotEmpty parameters.ReminderId ReminderIdIsRequired\n                            String.isNotEmpty parameters.After InvalidReminderDuration\n                        |]\n\n                    let command (context: HttpContext) (parameters: RescheduleReminderParameters) =\n                        task {\n                            let correlationId = getCorrelationId context\n                            let reminderId = Guid.Parse(parameters.ReminderId)\n\n                            match parseDuration parameters.After with\n                            | Some duration ->\n                                let newFireTime = getCurrentInstant () + duration\n                                let! reminderOption = getReminderById reminderId correlationId\n\n                                match reminderOption with\n                                | Some existingReminder ->\n                                    // Delete the old reminder and create a new one with updated time\n                                    let! _ = deleteReminder reminderId correlationId\n\n                                    let newReminder = { existingReminder with ReminderTime = newFireTime; CorrelationId = correlationId }\n\n                                    do! createReminder newReminder\n\n                                    return Ok $\"Reminder rescheduled to {formatInstantExtended newFireTime}.\"\n                                | None -> return Error \"Reminder not found.\"\n                            | None -> return Error $\"Invalid duration format: {parameters.After}. Use formats like '+15m', '+1h', '+1d'.\"\n                        }\n\n                    let! parameters = context |> parse<RescheduleReminderParameters>\n                    let! result = processCommand context parameters validations command\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; ReminderId: {reminderId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        parameters.ReminderId\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Creates a new manual reminder.\n    let Create: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: CreateReminderParameters) =\n                        [|\n                            String.isNotEmpty parameters.ActorName ReminderActorNameIsRequired\n                            String.isNotEmpty parameters.ActorId ReminderActorIdIsRequired\n                            String.isNotEmpty parameters.ReminderType InvalidReminderType\n                            String.isNotEmpty parameters.FireAt InvalidReminderTime\n                        |]\n\n                    let command (context: HttpContext) (parameters: CreateReminderParameters) =\n                        task {\n                            let correlationId = getCorrelationId context\n\n                            // Parse the fire time\n                            let parseResult = InstantPattern.ExtendedIso.Parse(parameters.FireAt)\n\n                            if parseResult.Success then\n                                let fireTime = parseResult.Value\n\n                                // Parse the reminder type\n                                let reminderTypeOption = discriminatedUnionFromString<ReminderTypes> parameters.ReminderType\n\n                                match reminderTypeOption with\n                                | Some reminderType ->\n                                    let reminderDto =\n                                        ReminderDto.Create\n                                            parameters.ActorName\n                                            parameters.ActorId\n                                            graceIds.OwnerId\n                                            graceIds.OrganizationId\n                                            graceIds.RepositoryId\n                                            reminderType\n                                            fireTime\n                                            ReminderState.EmptyReminderState\n                                            correlationId\n\n                                    do! createReminder reminderDto\n\n                                    return Ok $\"Reminder created with ID: {reminderDto.ReminderId}.\"\n                                | None -> return Error $\"Invalid reminder type: {parameters.ReminderType}.\"\n                            else\n                                return Error $\"Invalid fire time format: {parameters.FireAt}. Use ISO8601 format.\"\n                        }\n\n                    let! parameters = context |> parse<CreateReminderParameters>\n                    let! result = processCommand context parameters validations command\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n"
  },
  {
    "path": "src/Grace.Server/ReminderService.Server.fs",
    "content": "namespace Grace.Server\n\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Services\nopen Grace.Actors.Types\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Types.Reminder\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Hosting\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Linq\nopen System.Threading\nopen System.Threading.Tasks\nopen System.Text\nopen Microsoft.Azure.Cosmos\nopen Grace.Shared.Constants\nopen System.Net\n\nmodule ReminderService =\n\n    type ReminderValue() =\n        member val Id = String.Empty with get, set\n        member val PartitionKey = String.Empty with get, set\n        member val ReminderId = ReminderId.Empty with get, set\n        member val CorrelationId: CorrelationId = String.Empty with get, set\n        override this.ToString() = serialize this\n\n    type ReminderService() =\n        inherit BackgroundService()\n\n        let defaultReminderBatchSize = 1000\n        let timer = TimeSpan.FromSeconds(60.0)\n        let log = loggerFactory.CreateLogger(\"ReminderService.Server\")\n\n        let reminderBatchSize =\n            let mutable reminderBatchSize = 0\n\n            let enviromentValue =\n                Configuration()\n                    .Item(EnvironmentVariables.GraceReminderBatchSize)\n\n            if Int32.TryParse(enviromentValue, &reminderBatchSize) then\n                reminderBatchSize\n            else\n                defaultReminderBatchSize\n\n        /// Retrieves reminders from storage.\n        let retrieveReminders (cancellationToken: CancellationToken) =\n            task {\n                let reminders = List<ReminderValue>(reminderBatchSize)\n\n                match actorStateStorageProvider with\n                | Unknown -> ()\n                | AzureCosmosDb ->\n                    let queryDefinition =\n                        QueryDefinition(\n                            \"\"\"\n                            SELECT TOP @maxCount c.id as Id, c.State.Reminder.ReminderId AS ReminderId, c.State.Reminder.CorrelationId AS CorrelationId\n                            FROM c\n                            WHERE c.GrainType = @grainType\n                                AND c.PartitionKey = @partitionKey\n                                AND c.State.Reminder.ReminderTime < GetCurrentDateTime()\n                                ORDER BY c._ts ASC\n                            \"\"\"\n                        )\n                            .WithParameter(\"@maxCount\", reminderBatchSize)\n                            .WithParameter(\"@grainType\", StateName.Reminder)\n                            .WithParameter(\"@partitionKey\", StateName.Reminder)\n\n                    use iterator = ApplicationContext.cosmosContainer.GetItemQueryIterator<ReminderValue>(queryDefinition)\n\n                    while iterator.HasMoreResults do\n                        let! results = iterator.ReadNextAsync(cancellationToken)\n                        //logToConsole $\"*******Reminders retrieved: {results.Resource.Count()}.\"\n\n                        for reminder in results do\n                            reminders.Add(reminder)\n\n                | MongoDB -> ()\n\n                return reminders :> IReadOnlyList<ReminderValue>\n            }\n\n        /// Processes reminders by:\n        ///   1. retrieving them,\n        ///   2. calling .Remind() on each one to send the reminder to the source actor, and\n        ///   3. deleting the reminder from storage.\n        let processReminders (cancellationToken: CancellationToken) =\n            task {\n                let start = getCurrentInstant ()\n\n                try\n                    log.LogTrace(\n                        \"{CurrentInstant}: Node: {HostName}; In ReminderService.ProcessReminders. Retrieving reminders.\",\n                        getCurrentInstantExtended (),\n                        getMachineName\n                    )\n\n                    let! reminders = retrieveReminders cancellationToken\n\n                    if reminders.Count > 0 then\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; In ReminderService.ProcessReminders. Processing {reminderCount} reminder(s).\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            reminders.Count\n                        )\n\n                        do!\n                            Parallel.ForEachAsync(\n                                reminders,\n                                ParallelOptions,\n                                (fun reminder ct ->\n                                    ValueTask(\n                                        task {\n                                            try\n                                                // Insert random delay to smooth out the processing load.\n                                                do! Task.Delay(Random.Shared.Next(0, timer.Milliseconds))\n\n                                                let reminderActorProxy = Reminder.CreateActorProxy reminder.ReminderId reminder.CorrelationId\n\n                                                match! reminderActorProxy.Remind reminder.CorrelationId with\n                                                | Ok () ->\n                                                    let itemRequestOptions =\n                                                        ItemRequestOptions(\n                                                            PriorityLevel = PriorityLevel.Low,\n                                                            AddRequestHeaders =\n                                                                fun headers -> headers.Add(Constants.CorrelationIdHeaderKey, reminder.CorrelationId)\n                                                        )\n\n                                                    // Delete the reminder from storage to avoid reprocessing.\n                                                    do! reminderActorProxy.Delete reminder.CorrelationId\n                                                | Error error ->\n                                                    log.LogError(\n                                                        \"{CurrentInstant}: Node: {HostName}; Error processing reminder: {reminder.id}. {error}.\",\n                                                        getCurrentInstantExtended (),\n                                                        getMachineName,\n                                                        reminder.Id,\n                                                        error\n                                                    )\n                                            with\n                                            | ex ->\n                                                log.LogError(\n                                                    \"{CurrentInstant}: Node: {HostName}; Error processing reminder. Reminder: {Reminder}. Error: {error}.\",\n                                                    getCurrentInstantExtended (),\n                                                    getMachineName,\n                                                    reminder,\n                                                    (ExceptionResponse.Create ex)\n                                                )\n                                        }\n                                        :> Task\n                                    ))\n                            )\n                with\n                | ex ->\n                    log.LogError(\n                        \"{CurrentInstant}: Node: {HostName}; Error processing reminder. Error: {error}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        (ExceptionResponse.Create ex)\n                    )\n\n            }\n            :> Task\n\n        override this.StartAsync(cancellationToken: CancellationToken) =\n            log.LogInformation(\"{CurrentInstant}: Node: {HostName}; ReminderService is starting.\", getCurrentInstantExtended (), getMachineName)\n\n            log.LogInformation(\n                \"{CurrentInstant}: Node: {HostName}; Reminder batch size set to {reminderBatchSize}.\",\n                getCurrentInstantExtended (),\n                getMachineName,\n                reminderBatchSize\n            )\n\n            ``base``.StartAsync(cancellationToken)\n\n        override this.ExecuteAsync(stoppingToken: CancellationToken) =\n            task {\n                try\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; In ReminderService.ExecuteAsync. Pausing for {DelaySeconds} seconds before processing reminders.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        secondsToDelayReminderProcessing\n                    )\n\n                    // Initial delay before processing reminders; allowing the server to fully start up.\n                    do! Task.Delay(TimeSpan.FromSeconds(secondsToDelayReminderProcessing), stoppingToken)\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; In ReminderService.ExecuteAsync. Starting reminder timer; checking for reminders every {ReminderTimer} seconds.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        timer.TotalSeconds\n                    )\n\n                    use periodicTimer = new PeriodicTimer(timer)\n                    let mutable ticked = true\n                    let globalLockActorProxy = GlobalLock.CreateActorProxy LockName.ReminderLock (generateCorrelationId ())\n\n                    while ticked\n                          && not (stoppingToken.IsCancellationRequested) do\n                        let! locked = globalLockActorProxy.AcquireLock(getMachineName)\n\n                        if locked then\n                            do! processReminders (stoppingToken)\n\n                            match! globalLockActorProxy.ReleaseLock(getMachineName) with\n                            | Ok () -> ()\n                            | Error error ->\n                                log.LogError(\n                                    \"{CurrentInstant}: Node: {HostName}; Error releasing reminder lock: {error}.\",\n                                    getCurrentInstantExtended (),\n                                    getMachineName,\n                                    error\n                                )\n\n                            let! tick = periodicTimer.WaitForNextTickAsync(stoppingToken)\n                            ticked <- tick\n                        else\n                            do! Task.Delay(TimeSpan.FromSeconds(1.0), stoppingToken)\n                with\n                | :? OperationCanceledException -> ()\n                | ex ->\n                    log.LogError(\n                        \"{CurrentInstant}: Node: {HostName}; Error in ReminderService.ExecuteAsync. Error: {error}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        (ExceptionResponse.Create ex)\n                    )\n            }\n            :> Task\n\n        override this.StopAsync(cancellationToken: CancellationToken) =\n            log.LogInformation(\"{CurrentInstant}: Node: {HostName}; ReminderService is stopping.\", getCurrentInstantExtended (), getMachineName)\n            ``base``.StopAsync(cancellationToken)\n"
  },
  {
    "path": "src/Grace.Server/Repository.Server.fs",
    "content": "namespace Grace.Server\n\nopen FSharpPlus\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.Services\nopen Grace.Server.Validations\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Repository\nopen Grace.Types.Repository\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.AspNetCore.Mvc\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen OpenTelemetry.Trace\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Globalization\nopen System.Linq\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\nopen Microsoft.AspNetCore.Http.HttpResults\n\nmodule Repository =\n\n    type Validations<'T when 'T :> RepositoryParameters> = 'T -> ValueTask<Result<unit, RepositoryError>> array\n\n    let activitySource = new ActivitySource(\"Repository\")\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Repository.Server\")\n\n    let processCommand<'T when 'T :> RepositoryParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<RepositoryCommand>) =\n        task {\n            use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                let commandName = context.Items[\"Command\"] :?> string\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                // We know these Id's from ValidateIds.Middleware, so let's set them so we never have to resolve them again.\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let handleCommand organizationId repositoryId cmd =\n                    task {\n                        let actorProxy = Repository.CreateActorProxy organizationId repositoryId correlationId\n\n                        match! actorProxy.Handle cmd (createMetadata context) with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            log.LogDebug(\n                                \"{CurrentInstant}: In Branch.Server.handleCommand: error from actorProxy.Handle: {error}\",\n                                getCurrentInstantExtended (),\n                                (graceError.ToString())\n                            )\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                log.LogDebug(\n                    \"{CurrentInstant}: ****In Repository.Server.processCommand; about to run validations: CorrelationId: {correlationId}; RepositoryId: {repositoryId}.\",\n                    getCurrentInstantExtended (),\n                    correlationId,\n                    graceIds.RepositoryIdString\n                )\n\n                let validationResults = validations parameters\n\n                let! validationsPassed = validationResults |> allPass\n\n                log.LogDebug(\n                    \"{CurrentInstant}: ****In Repository.Server.processCommand: CorrelationId: {correlationId}; RepositoryId: {repositoryId}; validationsPassed: {validationsPassed}.\",\n                    getCurrentInstantExtended (),\n                    correlationId,\n                    graceIds.RepositoryIdString,\n                    validationsPassed\n                )\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let! result = handleCommand graceIds.OrganizationId graceIds.RepositoryId cmd\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; CorrelationId: {correlationId}; Finished {path}; Status code: {statusCode}; OwnerId: {ownerId}; OrganizationId: {organizationId}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        correlationId,\n                        context.Request.Path,\n                        context.Response.StatusCode,\n                        graceIds.OwnerIdString,\n                        graceIds.OrganizationIdString,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = getErrorOptionMessage error\n                    log.LogDebug(\"{CurrentInstant}: error: {error}\", getCurrentInstantExtended (), errorMessage)\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance(\"Path\", context.Request.Path.Value)\n                            .enhance (\"Error\", errorMessage)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Repository.Server.processCommand; Path: {path}; CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    context.Request.Path,\n                    (getCorrelationId context)\n                )\n\n                let graceError =\n                    (GraceError.Create $\"{Utilities.ExceptionResponse.Create ex}\" (getCorrelationId context))\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> RepositoryParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (maxCount: int)\n        (query: QueryResult<IRepositoryActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    // Get the actor proxy for the repository.\n                    let actorProxy = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n\n                    // Execute the query.\n                    let! queryResult = query context maxCount actorProxy\n\n                    // Wrap the query result in a GraceReturnValue.\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n\n                    let graceError =\n                        (GraceError.Create (getErrorOptionMessage error) correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Repository.Server.processQuery; Path: {path}; CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    context.Request.Path,\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.Create $\"{ExceptionResponse.Create ex}\" correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    /// Create a new repository.\n    let Create: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                context.Items.Add(\"Command\", nameof Create)\n                let graceIds = getGraceIds context\n\n                //let! parameters = context |> parse<CreateParameters>\n                let validations (parameters: CreateRepositoryParameters) =\n                    [|\n                        Repository.repositoryIdDoesNotExist graceIds.OrganizationId parameters.RepositoryId parameters.CorrelationId RepositoryIdAlreadyExists\n                        Repository.repositoryNameIsUnique\n                            parameters.OwnerId\n                            parameters.OrganizationId\n                            parameters.RepositoryName\n                            parameters.CorrelationId\n                            RepositoryNameAlreadyExists\n                    |]\n\n                let command (parameters: CreateRepositoryParameters) =\n                    task {\n                        return\n                            Create(\n                                RepositoryName parameters.RepositoryName,\n                                (Guid.Parse(parameters.RepositoryId)),\n                                graceIds.OwnerId,\n                                graceIds.OrganizationId,\n                                parameters.ObjectStorageProvider\n                            )\n                    }\n                    |> ValueTask<RepositoryCommand>\n\n                return! processCommand context validations command\n            }\n\n    /// Sets the search visibility of the repository.\n    let SetVisibility: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetRepositoryVisibilityParameters) =\n                    [|\n                        Repository.visibilityIsValid parameters.Visibility InvalidVisibilityValue\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetRepositoryVisibilityParameters) =\n                    SetRepositoryType(\n                        discriminatedUnionFromString<RepositoryType>(\n                            parameters.Visibility\n                        )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetRepositoryType)\n                return! processCommand context validations command\n            }\n\n    /// Sets the number of days to keep an entity that has been logically deleted. After this time expires, the entity will be physically deleted.\n    let SetLogicalDeleteDays: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetLogicalDeleteDaysParameters) =\n                    [|\n                        Repository.daysIsValid parameters.LogicalDeleteDays InvalidLogicalDeleteDaysValue\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetLogicalDeleteDaysParameters) =\n                    SetLogicalDeleteDays(parameters.LogicalDeleteDays)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetLogicalDeleteDays)\n                return! processCommand context validations command\n            }\n\n    /// Sets the number of days to keep saves in the repository.\n    let SetSaveDays: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetSaveDaysParameters) =\n                    [|\n                        Repository.daysIsValid parameters.SaveDays InvalidSaveDaysValue\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetSaveDaysParameters) =\n                    SetSaveDays(parameters.SaveDays)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetSaveDays)\n                return! processCommand context validations command\n            }\n\n    /// Sets the number of days to keep checkpoints in the repository.\n    let SetCheckpointDays: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetCheckpointDaysParameters) =\n                    [|\n                        Repository.daysIsValid parameters.CheckpointDays InvalidCheckpointDaysValue\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetCheckpointDaysParameters) =\n                    SetCheckpointDays(parameters.CheckpointDays)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetCheckpointDays)\n                return! processCommand context validations command\n            }\n\n    /// Sets the number of days to keep diff contents in the database.\n    let SetDiffCacheDays: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetDiffCacheDaysParameters) =\n                    [|\n                        Repository.daysIsValid parameters.DiffCacheDays InvalidDiffCacheDaysValue\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetDiffCacheDaysParameters) =\n                    SetDiffCacheDays(parameters.DiffCacheDays)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetDiffCacheDays)\n                return! processCommand context validations command\n            }\n\n    /// Sets the number of days to keep recursive directory version contents in the database.\n    let SetDirectoryVersionCacheDays: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetDirectoryVersionCacheDaysParameters) =\n                    [|\n                        Repository.daysIsValid parameters.DirectoryVersionCacheDays InvalidDirectoryVersionCacheDaysValue\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetDirectoryVersionCacheDaysParameters) =\n                    SetDirectoryVersionCacheDays(parameters.DirectoryVersionCacheDays)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetDirectoryVersionCacheDays)\n                return! processCommand context validations command\n            }\n\n    /// Sets the status of the repository (Public, Private).\n    let SetStatus: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetRepositoryStatusParameters) =\n                    [|\n                        String.isNotEmpty parameters.Status InvalidRepositoryStatus\n                        DiscriminatedUnion.isMemberOf<RepositoryStatus, RepositoryError> parameters.Status InvalidRepositoryStatus\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetRepositoryStatusParameters) =\n                    SetRepositoryStatus(\n                        discriminatedUnionFromString<RepositoryStatus>(\n                            parameters.Status\n                        )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetRepositoryStatus)\n                return! processCommand context validations command\n            }\n\n    /// Sets whether the repository allows large files.\n    let SetAllowsLargeFiles: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetAllowsLargeFilesParameters) =\n                    [|\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetAllowsLargeFilesParameters) =\n                    SetAllowsLargeFiles(parameters.AllowsLargeFiles)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetAllowsLargeFiles)\n                return! processCommand context validations command\n            }\n\n    /// Sets whether the repository allows anonymous access.\n    let SetAnonymousAccess: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetAnonymousAccessParameters) =\n                    [|\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetAnonymousAccessParameters) =\n                    SetAnonymousAccess(parameters.AnonymousAccess)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetAnonymousAccess)\n                return! processCommand context validations command\n            }\n\n    /// Sets the default server API version for the repository.\n    let SetDefaultServerApiVersion: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetDefaultServerApiVersionParameters) =\n                    [|\n                        String.isNotEmpty parameters.DefaultServerApiVersion InvalidServerApiVersion\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetDefaultServerApiVersionParameters) =\n                    SetDefaultServerApiVersion(parameters.DefaultServerApiVersion)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetDefaultServerApiVersion)\n                return! processCommand context validations command\n            }\n\n    /// Sets the conflict resolution policy for the repository.\n    let SetConflictResolutionPolicy: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetConflictResolutionPolicyParameters) =\n                    [|\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                        DiscriminatedUnion.isMemberOf<ConflictResolutionPolicy, RepositoryError>\n                            parameters.ConflictResolutionPolicy\n                            InvalidConflictResolutionPolicy\n                    |]\n\n                let command (parameters: SetConflictResolutionPolicyParameters) =\n                    SetConflictResolutionPolicy(\n                        discriminatedUnionFromString<ConflictResolutionPolicy>(\n                            parameters.ConflictResolutionPolicy\n                        )\n                            .Value\n                    )\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetConflictResolutionPolicy)\n                return! processCommand context validations command\n            }\n\n    /// Sets whether or not to keep saves in the repository.\n    let SetRecordSaves: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: RecordSavesParameters) =\n                    [|\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: RecordSavesParameters) =\n                    SetRecordSaves(parameters.RecordSaves)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetRecordSaves)\n                return! processCommand context validations command\n            }\n\n    /// Sets the description of the repository.\n    let SetDescription: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetRepositoryDescriptionParameters) =\n                    [|\n                        String.isNotEmpty parameters.Description DescriptionIsRequired\n                        String.maxLength parameters.Description 2048 DescriptionIsTooLong\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: SetRepositoryDescriptionParameters) =\n                    SetDescription(parameters.Description)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetDescription)\n                return! processCommand context validations command\n            }\n\n    /// Sets the name of the repository.\n    let SetName: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: SetRepositoryNameParameters) =\n                    [|\n                        String.isNotEmpty parameters.NewName RepositoryNameIsRequired\n                        String.isValidGraceName parameters.NewName InvalidRepositoryName\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                        Repository.repositoryNameIsUnique\n                            parameters.OwnerId\n                            parameters.OrganizationId\n                            parameters.NewName\n                            parameters.CorrelationId\n                            RepositoryNameAlreadyExists\n                    |]\n\n                let command (parameters: SetRepositoryNameParameters) = SetName(parameters.NewName) |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof SetName)\n                return! processCommand context validations command\n            }\n\n    /// Deletes the repository.\n    let Delete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: DeleteRepositoryParameters) =\n                    [|\n                        String.isNotEmpty parameters.DeleteReason DeleteReasonIsRequired\n                        Repository.repositoryIsNotDeleted context parameters.CorrelationId RepositoryIsDeleted\n                    |]\n\n                let command (parameters: DeleteRepositoryParameters) =\n                    DeleteLogical(parameters.Force, parameters.DeleteReason)\n                    |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof DeleteLogical)\n                return! processCommand context validations command\n            }\n\n    /// Undeletes a previously-deleted repository.\n    let Undelete: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: RepositoryParameters) =\n                    [|\n                        Repository.repositoryIsDeleted context parameters.CorrelationId RepositoryIsNotDeleted\n                    |]\n\n                let command (parameters: RepositoryParameters) = Undelete |> returnValueTask\n\n                context.Items.Add(\"Command\", nameof Undelete)\n                return! processCommand context validations command\n            }\n\n    /// Checks if a repository exists with the given parameters.\n    let Exists: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = context.Items[nameof GraceIds] :?> GraceIds\n\n                try\n                    let validations (parameters: RepositoryParameters) = [||]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IRepositoryActor) =\n                        task { return! actorProxy.Exists(getCorrelationId context) }\n\n                    let! parameters = context |> parse<RepositoryParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Checks if a repository is empty - that is, just created - or not.\n    let IsEmpty: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = context.Items[nameof GraceIds] :?> GraceIds\n\n                try\n                    let validations (parameters: RepositoryParameters) = [||]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IRepositoryActor) =\n                        task { return! actorProxy.IsEmpty(getCorrelationId context) }\n\n                    let! parameters = context |> parse<RepositoryParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Gets a repository.\n    let Get: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: RepositoryParameters) = [||]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IRepositoryActor) = actorProxy.Get(getCorrelationId context)\n\n                    let! parameters = context |> parse<RepositoryParameters>\n                    let! result = processQuery context parameters validations 1 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Gets a repository's branches.\n    let GetBranches: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = context.Items[nameof GraceIds] :?> GraceIds\n\n                try\n                    let validations (parameters: GetBranchesParameters) =\n                        [|\n                            Number.isWithinRange parameters.MaxCount 1 1000 InvalidMaxCountValue\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IRepositoryActor) =\n                        task {\n                            let graceIds = context.Items[nameof GraceIds] :?> GraceIds\n                            let includeDeleted = context.Items[\"IncludeDeleted\"] :?> bool\n\n                            return!\n                                getBranches graceIds.OwnerId graceIds.OrganizationId graceIds.RepositoryId maxCount includeDeleted (getCorrelationId context)\n                        }\n\n                    let! parameters = context |> parse<GetBranchesParameters>\n                    context.Items.Add(\"IncludeDeleted\", parameters.IncludeDeleted)\n                    let! result = processQuery context parameters validations 1000 query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Gets a list of references, given a list of reference IDs.\n    let GetReferencesByReferenceId: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = getGraceIds context\n\n                try\n                    let validations (parameters: GetReferencesByReferenceIdParameters) =\n                        [|\n                            Input.listIsNonEmpty parameters.ReferenceIds ReferenceIdsAreRequired\n                            Number.isWithinRange parameters.MaxCount 1 1000 InvalidMaxCountValue\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IRepositoryActor) =\n                        task {\n                            let referenceIds = context.Items[\"ReferenceIds\"] :?> IEnumerable<ReferenceId>\n\n                            log.LogDebug(\"In Repository.Server.GetReferencesByReferenceId: ReferenceIds: {referenceIds}\", serialize referenceIds)\n\n                            return! getReferencesByReferenceId graceIds.RepositoryId referenceIds maxCount (getCorrelationId context)\n                        }\n\n                    let! parameters =\n                        context\n                        |> parse<GetReferencesByReferenceIdParameters>\n\n                    context.Items.Add(\"ReferenceIds\", parameters.ReferenceIds)\n                    let! result = processQuery context parameters validations parameters.MaxCount query\n\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogInformation(\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return result\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n\n    /// Gets a list of branches, given a list of branch IDs.\n    let GetBranchesByBranchId: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let startTime = getCurrentInstant ()\n                let graceIds = context.Items[nameof GraceIds] :?> GraceIds\n\n                try\n                    let validations (parameters: GetBranchesByBranchIdParameters) =\n                        [|\n                            Input.listIsNonEmpty parameters.BranchIds BranchIdsAreRequired\n                            Number.isWithinRange parameters.MaxCount 1 1000 InvalidMaxCountValue\n                        |]\n\n                    let query (context: HttpContext) (maxCount: int) (actorProxy: IRepositoryActor) =\n                        task {\n                            let repositoryId = Guid.Parse(graceIds.RepositoryIdString)\n                            let branchIdsFromContext = (context.Items[\"BranchIds\"] :?> string)\n\n                            let branchIds =\n                                branchIdsFromContext\n                                    .Split(',', StringSplitOptions.TrimEntries)\n                                    .Select(fun branchId -> Guid.Parse(branchId))\n\n                            let includeDeleted = context.Items[\"IncludeDeleted\"] :?> bool\n                            return! getBranchesByBranchId repositoryId branchIds maxCount includeDeleted\n                        }\n\n                    let! parameters = context |> parse<GetBranchesByBranchIdParameters>\n\n                    let branchIdList = parameters.BranchIds.ToList() // We need .Count below, so may as well materialize it once here.\n                    let sb = stringBuilderPool.Get()\n\n                    try\n                        let branchIds = branchIdList.Aggregate(sb, (fun sb branchId -> sb.Append($\"{branchId},\")))\n\n                        context.Items.Add(\"BranchIds\", (branchIds.ToString())[0..^1])\n                        context.Items.Add(\"IncludeDeleted\", parameters.IncludeDeleted)\n\n                        let! result = processQuery context parameters validations (branchIdList.Count) query\n\n                        let duration_ms = getDurationRightAligned_ms startTime\n\n                        log.LogInformation(\n                            \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Finished {path}; RepositoryId: {repositoryId}.\",\n                            getCurrentInstantExtended (),\n                            getMachineName,\n                            duration_ms,\n                            (getCorrelationId context),\n                            context.Request.Path,\n                            graceIds.RepositoryIdString\n                        )\n\n                        return result\n                    finally\n                        stringBuilderPool.Return(sb)\n                with\n                | ex ->\n                    let duration_ms = getDurationRightAligned_ms startTime\n\n                    log.LogError(\n                        ex,\n                        \"{CurrentInstant}: Node: {HostName}; Duration: {duration_ms}ms; CorrelationId: {correlationId}; Error in {path}; RepositoryId: {repositoryId}.\",\n                        getCurrentInstantExtended (),\n                        getMachineName,\n                        duration_ms,\n                        (getCorrelationId context),\n                        context.Request.Path,\n                        graceIds.RepositoryIdString\n                    )\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create $\"{ExceptionResponse.Create ex}\" (getCorrelationId context))\n            }\n"
  },
  {
    "path": "src/Grace.Server/Review.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Review\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.PromotionSet\nopen Grace.Types.Queue\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule Review =\n    type Validations<'T when 'T :> ReviewParameters> = 'T -> ValueTask<Result<unit, ReviewError>> array\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Review.Server\")\n\n    let activitySource = new ActivitySource(\"Review\")\n\n    let private resolveCandidatePromotionSetWith\n        (resolvePromotionSet: Guid -> Task<Grace.Types.PromotionSet.PromotionSetDto option>)\n        (parameters: CandidateProjectionParameters)\n        =\n        task {\n            let normalizedCandidateId = ReviewModels.normalizeCandidateId parameters.CandidateId\n\n            match ReviewModels.tryParseCandidateId parameters.CandidateId with\n            | Error _ ->\n                let graceError =\n                    (GraceError.Create \"CandidateId must be a valid non-empty Guid.\" parameters.CorrelationId)\n                        .enhance(\"CandidateId\", parameters.CandidateId)\n                        .enhance(\"NormalizedCandidateId\", normalizedCandidateId)\n                        .enhance (nameof RepositoryId, parameters.RepositoryId)\n\n                return Error graceError\n            | Ok (candidateGuid, canonicalCandidateId) ->\n                let! promotionSet = resolvePromotionSet candidateGuid\n\n                match promotionSet with\n                | Option.Some promotionSet -> return Ok(candidateGuid, canonicalCandidateId, promotionSet)\n                | Option.None ->\n                    let graceError =\n                        (GraceError.Create $\"Candidate '{canonicalCandidateId}' was not found in repository scope.\" parameters.CorrelationId)\n                            .enhance(\"CandidateId\", parameters.CandidateId)\n                            .enhance(\"NormalizedCandidateId\", canonicalCandidateId)\n                            .enhance (nameof RepositoryId, parameters.RepositoryId)\n\n                    return Error graceError\n        }\n\n    let private resolvePromotionSetById (context: HttpContext) (promotionSetId: Guid) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = PromotionSet.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n            let! exists = actorProxy.Exists correlationId\n\n            if exists then\n                let! promotionSet = actorProxy.Get correlationId\n                return Option.Some promotionSet\n            else\n                return Option.None\n        }\n\n    let private resolveCandidateProjectionContext (context: HttpContext) (parameters: CandidateProjectionParameters) =\n        task {\n            match! resolveCandidatePromotionSetWith (resolvePromotionSetById context) parameters with\n            | Error error -> return Error error\n            | Ok (_, normalizedCandidateId, promotionSet) ->\n                let identity =\n                    ReviewModels.createCandidateIdentityProjection\n                        normalizedCandidateId\n                        promotionSet.PromotionSetId\n                        parameters.OwnerId\n                        parameters.OrganizationId\n                        parameters.RepositoryId\n\n                return Ok(identity, promotionSet)\n        }\n\n    let private tryGetQueueForPromotionSet (context: HttpContext) (promotionSet: PromotionSetDto) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = PromotionQueue.CreateActorProxy promotionSet.TargetBranchId graceIds.RepositoryId correlationId\n            let! exists = actorProxy.Exists correlationId\n\n            if exists then\n                let! queue = actorProxy.Get correlationId\n                return Option.Some queue\n            else\n                return Option.None\n        }\n\n    let private getReviewStateForPromotionSet (context: HttpContext) (promotionSetId: PromotionSetId) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = Review.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n            let! notes = actorProxy.GetNotes correlationId\n            let! checkpoints = actorProxy.GetCheckpoints correlationId\n            return notes, (checkpoints |> Seq.toList)\n        }\n\n    let internal deriveCandidateRequiredActions\n        (promotionSetStatus: PromotionSetStatus)\n        (stepsComputationStatus: StepsComputationStatus)\n        (queueState: QueueState option)\n        (unresolvedFindingCount: int)\n        (validationSummaryAvailable: bool)\n        =\n        let requiredActions = ResizeArray<string>()\n        let diagnostics = ResizeArray<string>()\n\n        if stepsComputationStatus = StepsComputationStatus.NotComputed\n           || stepsComputationStatus = StepsComputationStatus.ComputeFailed then\n            requiredActions.Add(\"RetryComputation\")\n\n        if promotionSetStatus = PromotionSetStatus.Blocked then\n            requiredActions.Add(\"ResolveConflicts\")\n\n        match queueState with\n        | Option.Some QueueState.Paused -> requiredActions.Add(\"ResumeQueue\")\n        | Option.Some QueueState.Degraded -> requiredActions.Add(\"RepairQueue\")\n        | Option.Some _ -> ()\n        | Option.None -> diagnostics.Add(\"Queue state is unavailable for this candidate.\")\n\n        if unresolvedFindingCount > 0 then requiredActions.Add(\"ResolveFindings\")\n\n        if not validationSummaryAvailable then\n            requiredActions.Add(\"ConfirmValidationSummary\")\n\n        if requiredActions.Count = 0 then requiredActions.Add(\"NoActionRequired\")\n\n        requiredActions |> Seq.distinct |> Seq.toList, diagnostics |> Seq.toList\n\n    let private buildCandidateProjectionSourceStates (queueExists: bool) (reviewNotesExists: bool) =\n        [\n            ReviewModels.createProjectionSourceStateMetadata \"identity\" ProjectionSourceStates.Authoritative \"Resolved from candidate identity projection.\"\n            ReviewModels.createProjectionSourceStateMetadata \"promotionSet\" ProjectionSourceStates.Authoritative \"Resolved from PromotionSet.Get.\"\n            if queueExists then\n                ReviewModels.createProjectionSourceStateMetadata \"queue\" ProjectionSourceStates.Authoritative \"Resolved from Queue.Get.\"\n            else\n                ReviewModels.createProjectionSourceStateMetadata\n                    \"queue\"\n                    ProjectionSourceStates.NotAvailable\n                    \"Queue is not initialized for candidate target branch.\"\n            if reviewNotesExists then\n                ReviewModels.createProjectionSourceStateMetadata \"review\" ProjectionSourceStates.Authoritative \"Resolved from Review.GetNotes.\"\n            else\n                ReviewModels.createProjectionSourceStateMetadata\n                    \"review\"\n                    ProjectionSourceStates.NotAvailable\n                    \"Review notes are not available for this candidate.\"\n        ]\n\n    let internal buildCandidateProjectionSnapshot\n        (identity: CandidateIdentityProjection)\n        (promotionSet: PromotionSetDto)\n        (queue: PromotionQueue option)\n        (reviewNotes: ReviewNotes option)\n        =\n        let unresolvedFindingCount =\n            reviewNotes\n            |> Option.map (fun notes ->\n                notes.Findings\n                |> List.filter (fun finding ->\n                    finding.ResolutionState\n                    <> FindingResolutionState.Approved)\n                |> List.length)\n            |> Option.defaultValue 0\n\n        let validationSummaryAvailable =\n            reviewNotes\n            |> Option.bind (fun notes -> notes.ValidationSummary)\n            |> Option.isSome\n\n        let requiredActions, diagnostics =\n            deriveCandidateRequiredActions\n                promotionSet.Status\n                promotionSet.StepsComputationStatus\n                (queue |> Option.map (fun value -> value.State))\n                unresolvedFindingCount\n                validationSummaryAvailable\n\n        let queueDiagnostics =\n            if reviewNotes.IsSome then\n                diagnostics\n            else\n                diagnostics\n                @ [\n                    \"Review notes are not available for this candidate.\"\n                ]\n\n        let result = CandidateProjectionSnapshotResult()\n        result.Identity <- identity\n        result.PromotionSetStatus <- getDiscriminatedUnionCaseName promotionSet.Status\n        result.StepsComputationStatus <- getDiscriminatedUnionCaseName promotionSet.StepsComputationStatus\n\n        result.QueueState <-\n            queue\n            |> Option.map (fun value -> getDiscriminatedUnionCaseName value.State)\n            |> Option.defaultValue ProjectionSourceStates.NotAvailable\n\n        result.RunningPromotionSetId <-\n            queue\n            |> Option.bind (fun value -> value.RunningPromotionSetId)\n            |> Option.map string\n            |> Option.defaultValue String.Empty\n\n        result.UnresolvedFindingCount <- unresolvedFindingCount\n        result.ValidationSummaryAvailable <- validationSummaryAvailable\n        result.RequiredActions <- requiredActions\n        result.Diagnostics <- queueDiagnostics\n        result.SourceStates <- buildCandidateProjectionSourceStates queue.IsSome reviewNotes.IsSome\n        result\n\n    let internal buildCandidateAttestationEntries (policySnapshotId: string option) (latestCheckpoint: ReviewCheckpoint option) =\n        let policyAttestation = CandidateAttestation(Name = \"PolicySnapshot\")\n\n        match policySnapshotId with\n        | Option.Some snapshotId ->\n            policyAttestation.Status <- ProjectionSourceStates.Authoritative\n            policyAttestation.Detail <- $\"Policy snapshot '{snapshotId}' is available.\"\n        | Option.None ->\n            policyAttestation.Status <- ProjectionSourceStates.NotAvailable\n            policyAttestation.Detail <- \"Policy snapshot context is unavailable for this candidate.\"\n\n        let checkpointAttestation = CandidateAttestation(Name = \"ReviewCheckpoint\")\n\n        match latestCheckpoint with\n        | Option.Some checkpoint ->\n            checkpointAttestation.Status <- ProjectionSourceStates.Authoritative\n\n            checkpointAttestation.Detail <- $\"Checkpoint '{checkpoint.ReviewCheckpointId}' by '{checkpoint.Reviewer}' at '{checkpoint.Timestamp}'.\"\n        | Option.None ->\n            checkpointAttestation.Status <- ProjectionSourceStates.NotAvailable\n            checkpointAttestation.Detail <- \"No review checkpoint is available for this candidate.\"\n\n        let diagnostics =\n            [\n                if policySnapshotId.IsNone then\n                    \"Policy snapshot context is unavailable for this candidate.\"\n                if latestCheckpoint.IsNone then\n                    \"Review checkpoint context is unavailable for this candidate.\"\n            ]\n\n        let sourceStates =\n            [\n                ReviewModels.createProjectionSourceStateMetadata \"identity\" ProjectionSourceStates.Authoritative \"Resolved from candidate identity projection.\"\n                ReviewModels.createProjectionSourceStateMetadata\n                    \"policy\"\n                    (if policySnapshotId.IsSome then\n                         ProjectionSourceStates.Authoritative\n                     else\n                         ProjectionSourceStates.NotAvailable)\n                    \"Resolved from Policy.GetCurrent.\"\n                ReviewModels.createProjectionSourceStateMetadata\n                    \"checkpoint\"\n                    (if latestCheckpoint.IsSome then\n                         ProjectionSourceStates.Authoritative\n                     else\n                         ProjectionSourceStates.NotAvailable)\n                    \"Resolved from Review.GetCheckpoints.\"\n            ]\n\n        [\n            policyAttestation\n            checkpointAttestation\n        ],\n        diagnostics,\n        sourceStates\n\n    let private findingSeverityRank (severity: FindingSeverity) =\n        match severity with\n        | FindingSeverity.Critical -> 0\n        | FindingSeverity.High -> 1\n        | FindingSeverity.Medium -> 2\n        | FindingSeverity.Low -> 3\n        | FindingSeverity.Info -> 4\n\n    let private requiredActionRank (action: string) =\n        match action with\n        | \"ResolveConflicts\" -> 0\n        | \"ResolveFindings\" -> 1\n        | \"RetryComputation\" -> 2\n        | \"ResumeQueue\" -> 3\n        | \"RepairQueue\" -> 4\n        | \"ConfirmValidationSummary\" -> 5\n        | \"NoActionRequired\" -> 6\n        | _ -> 99\n\n    let private blockerSeverityRank (severity: string) =\n        match severity with\n        | \"Critical\" -> 0\n        | \"High\" -> 1\n        | \"Medium\" -> 2\n        | \"Low\" -> 3\n        | \"Info\" -> 4\n        | _ -> 99\n\n    let private reportSectionRank (section: string) =\n        match section with\n        | value when value = ReviewModels.ReviewReportSections.CandidateAndPromotionSet -> 0\n        | value when value = ReviewModels.ReviewReportSections.QueueAndRequiredActions -> 1\n        | value when value = ReviewModels.ReviewReportSections.ValidationAndGateOutcomes -> 2\n        | value when value = ReviewModels.ReviewReportSections.ReviewNotesAndCheckpoint -> 3\n        | value when value = ReviewModels.ReviewReportSections.WorkItemLinksAndArtifacts -> 4\n        | value when value = ReviewModels.ReviewReportSections.BlockingReasonsAndNextActions -> 5\n        | _ -> 99\n\n    let private actionCategoryRank (category: string) =\n        match category with\n        | \"candidate\" -> 0\n        | \"queue\" -> 1\n        | \"promotion-set\" -> 2\n        | \"review\" -> 3\n        | _ -> 99\n\n    let private sortSourceStates (sourceStates: ProjectionSourceStateMetadata list) =\n        sourceStates\n        |> List.sortBy (fun sourceState -> sourceState.Section, sourceState.SourceState, sourceState.Detail)\n\n    let private mapRequiredActionToBlockerAndSuggestion (candidateId: string) (promotionSetId: string) (targetBranchId: string) (action: string) =\n        match action with\n        | \"ResolveConflicts\" ->\n            Option.Some(\n                \"Critical\",\n                ReviewModels.ReviewReportSections.CandidateAndPromotionSet,\n                \"Promotion set is blocked by unresolved conflicts.\",\n                \"promotion-set\",\n                $\"grace promotion-set conflicts show --promotion-set {promotionSetId}\"\n            )\n        | \"ResolveFindings\" ->\n            Option.Some(\n                \"High\",\n                ReviewModels.ReviewReportSections.ReviewNotesAndCheckpoint,\n                \"Review findings require resolution before candidate promotion can continue.\",\n                \"review\",\n                $\"grace review open --promotion-set {promotionSetId}\"\n            )\n        | \"RetryComputation\" ->\n            Option.Some(\n                \"High\",\n                ReviewModels.ReviewReportSections.CandidateAndPromotionSet,\n                \"Promotion set computation is stale or failed and requires recomputation.\",\n                \"candidate\",\n                $\"grace candidate retry --candidate {candidateId}\"\n            )\n        | \"ResumeQueue\" ->\n            Option.Some(\n                \"Medium\",\n                ReviewModels.ReviewReportSections.QueueAndRequiredActions,\n                \"Target branch queue is paused.\",\n                \"queue\",\n                $\"grace queue resume --branch {targetBranchId}\"\n            )\n        | \"RepairQueue\" ->\n            Option.Some(\n                \"Medium\",\n                ReviewModels.ReviewReportSections.QueueAndRequiredActions,\n                \"Target branch queue is degraded and needs operator attention.\",\n                \"queue\",\n                $\"grace queue status --branch {targetBranchId}\"\n            )\n        | \"ConfirmValidationSummary\" ->\n            Option.Some(\n                \"Low\",\n                ReviewModels.ReviewReportSections.ValidationAndGateOutcomes,\n                \"Validation summary is unavailable for this candidate.\",\n                \"review\",\n                $\"grace review open --promotion-set {promotionSetId}\"\n            )\n        | \"NoActionRequired\" -> Option.None\n        | _ ->\n            Option.Some(\n                \"Low\",\n                ReviewModels.ReviewReportSections.BlockingReasonsAndNextActions,\n                $\"Unknown required action '{action}' was reported.\",\n                \"review\",\n                $\"grace review report show --candidate {candidateId}\"\n            )\n\n    let internal buildReviewReport\n        (identity: CandidateIdentityProjection)\n        (promotionSet: PromotionSetDto)\n        (snapshot: CandidateProjectionSnapshotResult)\n        (reviewNotes: ReviewNotes option)\n        (checkpoints: ReviewCheckpoint list)\n        =\n        let promotionSetId = promotionSet.PromotionSetId.ToString()\n        let targetBranchId = promotionSet.TargetBranchId.ToString()\n\n        let requiredActions =\n            snapshot.RequiredActions\n            |> List.sortBy requiredActionRank\n\n        let queueDiagnostics = snapshot.Diagnostics |> List.sort\n\n        let identitySectionSourceStates =\n            snapshot.SourceStates\n            |> List.filter (fun sourceState ->\n                sourceState.Section = \"identity\"\n                || sourceState.Section = \"promotionSet\")\n            |> sortSourceStates\n\n        let identitySection =\n            ReviewModels.createReviewReportSection\n                ReviewModels.ReviewReportSections.CandidateAndPromotionSet\n                \"Candidate and PromotionSet identity or status\"\n                ProjectionSourceStates.Authoritative\n                identitySectionSourceStates\n                [\n                    ReviewModels.createReviewReportEntry \"CandidateId\" [ identity.CandidateId ]\n                    ReviewModels.createReviewReportEntry \"PromotionSetId\" [ identity.PromotionSetId ]\n                    ReviewModels.createReviewReportEntry \"IdentityMode\" [ identity.IdentityMode ]\n                    ReviewModels.createReviewReportEntry \"PromotionSetStatus\" [ snapshot.PromotionSetStatus ]\n                    ReviewModels.createReviewReportEntry \"StepsComputationStatus\" [ snapshot.StepsComputationStatus ]\n                ]\n                []\n\n        let queueSourceState =\n            snapshot.SourceStates\n            |> List.tryFind (fun sourceState -> sourceState.Section = \"queue\")\n            |> Option.map (fun sourceState -> sourceState.SourceState)\n            |> Option.defaultValue ProjectionSourceStates.NotAvailable\n\n        let queueSectionSourceStates =\n            snapshot.SourceStates\n            |> List.filter (fun sourceState ->\n                sourceState.Section = \"queue\"\n                || sourceState.Section = \"identity\")\n            |> sortSourceStates\n\n        let queueSection =\n            ReviewModels.createReviewReportSection\n                ReviewModels.ReviewReportSections.QueueAndRequiredActions\n                \"Queue state and required actions\"\n                queueSourceState\n                queueSectionSourceStates\n                [\n                    ReviewModels.createReviewReportEntry \"QueueState\" [ snapshot.QueueState ]\n                    ReviewModels.createReviewReportEntry\n                        \"RunningPromotionSetId\"\n                        [\n                            if String.IsNullOrWhiteSpace(snapshot.RunningPromotionSetId) then\n                                ProjectionSourceStates.NotAvailable\n                            else\n                                snapshot.RunningPromotionSetId\n                        ]\n                    ReviewModels.createReviewReportEntry \"RequiredActions\" requiredActions\n                ]\n                queueDiagnostics\n\n        let validationSummary =\n            reviewNotes\n            |> Option.bind (fun notes -> notes.ValidationSummary)\n\n        let validationResultIds =\n            validationSummary\n            |> Option.map (fun summary ->\n                summary.ValidationResultIds\n                |> List.map string\n                |> List.sort)\n            |> Option.defaultValue [ ProjectionSourceStates.NotAvailable ]\n\n        let gateOutcomes =\n            requiredActions\n            |> List.filter (fun action -> action <> \"NoActionRequired\")\n            |> List.map (fun action -> $\"ActionRequired:{action}\")\n            |> function\n                | [] -> [ \"NoGateBlockersDetected\" ]\n                | values -> values\n\n        let validationSourceState =\n            if validationSummary.IsSome then\n                ProjectionSourceStates.Authoritative\n            else\n                ProjectionSourceStates.NotAvailable\n\n        let validationSectionSourceStates =\n            [\n                ReviewModels.createProjectionSourceStateMetadata \"validation\" validationSourceState \"Resolved from ReviewNotes.ValidationSummary.\"\n            ]\n            |> sortSourceStates\n\n        let validationDiagnostics =\n            if validationSummary.IsSome then\n                []\n            else\n                [\n                    \"Validation summary is not available for this candidate.\"\n                ]\n\n        let validationSection =\n            ReviewModels.createReviewReportSection\n                ReviewModels.ReviewReportSections.ValidationAndGateOutcomes\n                \"Validation results and gate-like outcomes\"\n                validationSourceState\n                validationSectionSourceStates\n                [\n                    ReviewModels.createReviewReportEntry\n                        \"ValidationSummary\"\n                        [\n                            validationSummary\n                            |> Option.map (fun summary ->\n                                if String.IsNullOrWhiteSpace(summary.Summary) then\n                                    ProjectionSourceStates.NotAvailable\n                                else\n                                    summary.Summary)\n                            |> Option.defaultValue ProjectionSourceStates.NotAvailable\n                        ]\n                    ReviewModels.createReviewReportEntry \"ValidationResultIds\" validationResultIds\n                    ReviewModels.createReviewReportEntry \"GateOutcomes\" gateOutcomes\n                ]\n                validationDiagnostics\n\n        let latestCheckpoint =\n            checkpoints\n            |> List.sortByDescending (fun checkpoint -> checkpoint.Timestamp, checkpoint.ReviewCheckpointId)\n            |> List.tryHead\n\n        let reviewSourceState =\n            if reviewNotes.IsSome then\n                ProjectionSourceStates.Authoritative\n            else\n                ProjectionSourceStates.NotAvailable\n\n        let findingsBySeverity =\n            reviewNotes\n            |> Option.map (fun notes ->\n                notes.Findings\n                |> List.groupBy (fun finding -> finding.Severity)\n                |> List.sortBy (fun (severity, _) -> findingSeverityRank severity)\n                |> List.map (fun (severity, findings) ->\n                    let unresolvedCount =\n                        findings\n                        |> List.filter (fun finding ->\n                            finding.ResolutionState\n                            <> FindingResolutionState.Approved)\n                        |> List.length\n\n                    $\"{getDiscriminatedUnionCaseName severity}: total={findings.Length}; unresolved={unresolvedCount}\"))\n            |> Option.defaultValue [ ProjectionSourceStates.NotAvailable ]\n\n        let reviewDiagnostics =\n            [\n                if reviewNotes.IsNone then \"Review notes are not available for this candidate.\"\n                if latestCheckpoint.IsNone then\n                    \"Review checkpoint context is unavailable for this candidate.\"\n            ]\n\n        let reviewSectionSourceStates =\n            [\n                ReviewModels.createProjectionSourceStateMetadata \"review\" reviewSourceState \"Resolved from Review.GetNotes.\"\n                ReviewModels.createProjectionSourceStateMetadata\n                    \"checkpoint\"\n                    (if latestCheckpoint.IsSome then\n                         ProjectionSourceStates.Authoritative\n                     else\n                         ProjectionSourceStates.NotAvailable)\n                    \"Resolved from Review.GetCheckpoints.\"\n            ]\n            |> sortSourceStates\n\n        let reviewSection =\n            ReviewModels.createReviewReportSection\n                ReviewModels.ReviewReportSections.ReviewNotesAndCheckpoint\n                \"Review notes, findings, and checkpoint summary\"\n                reviewSourceState\n                reviewSectionSourceStates\n                [\n                    ReviewModels.createReviewReportEntry\n                        \"ReviewNotesId\"\n                        [\n                            reviewNotes\n                            |> Option.map (fun notes -> notes.ReviewNotesId.ToString())\n                            |> Option.defaultValue ProjectionSourceStates.NotAvailable\n                        ]\n                    ReviewModels.createReviewReportEntry\n                        \"ReviewSummary\"\n                        [\n                            reviewNotes\n                            |> Option.map (fun notes ->\n                                if String.IsNullOrWhiteSpace(notes.Summary) then\n                                    ProjectionSourceStates.NotAvailable\n                                else\n                                    notes.Summary)\n                            |> Option.defaultValue ProjectionSourceStates.NotAvailable\n                        ]\n                    ReviewModels.createReviewReportEntry\n                        \"FindingsTotal\"\n                        [\n                            reviewNotes\n                            |> Option.map (fun notes -> notes.Findings.Length.ToString())\n                            |> Option.defaultValue \"0\"\n                        ]\n                    ReviewModels.createReviewReportEntry\n                        \"FindingsUnresolved\"\n                        [\n                            snapshot.UnresolvedFindingCount.ToString()\n                        ]\n                    ReviewModels.createReviewReportEntry \"FindingsBySeverity\" findingsBySeverity\n                    ReviewModels.createReviewReportEntry\n                        \"LatestCheckpoint\"\n                        [\n                            latestCheckpoint\n                            |> Option.map (fun checkpoint ->\n                                $\"Checkpoint '{checkpoint.ReviewCheckpointId}' by '{checkpoint.Reviewer}' at '{checkpoint.Timestamp}'.\")\n                            |> Option.defaultValue ProjectionSourceStates.NotAvailable\n                        ]\n                ]\n                reviewDiagnostics\n\n        let workItemDiagnostics =\n            [\n                \"Work item reverse lookup is not available for candidate projections.\"\n                \"Artifact summary is unavailable without authoritative work-item links.\"\n            ]\n\n        let workItemSection =\n            ReviewModels.createReviewReportSection\n                ReviewModels.ReviewReportSections.WorkItemLinksAndArtifacts\n                \"Work-item links and artifact summary\"\n                ProjectionSourceStates.NotAvailable\n                [\n                    ReviewModels.createProjectionSourceStateMetadata\n                        \"workItems\"\n                        ProjectionSourceStates.NotAvailable\n                        \"Candidate to WorkItem reverse lookup endpoint is not available.\"\n                ]\n                [\n                    ReviewModels.createReviewReportEntry \"LinkedWorkItems\" [ ProjectionSourceStates.NotAvailable ]\n                    ReviewModels.createReviewReportEntry \"ArtifactSummary\" [ ProjectionSourceStates.NotAvailable ]\n                ]\n                workItemDiagnostics\n\n        let requiredActionBlockers =\n            requiredActions\n            |> List.choose (mapRequiredActionToBlockerAndSuggestion identity.CandidateId promotionSetId targetBranchId)\n            |> List.distinct\n            |> List.sortBy (fun (severity, section, reason, _, _) -> blockerSeverityRank severity, reportSectionRank section, reason)\n\n        let diagnosticBlockers =\n            (queueDiagnostics\n             @ validationDiagnostics\n               @ reviewDiagnostics @ workItemDiagnostics)\n            |> List.distinct\n            |> List.sort\n            |> List.map (fun diagnostic ->\n                \"Low\",\n                ReviewModels.ReviewReportSections.BlockingReasonsAndNextActions,\n                diagnostic,\n                \"review\",\n                $\"grace review report show --candidate {identity.CandidateId}\")\n\n        let blockers =\n            requiredActionBlockers @ diagnosticBlockers\n            |> List.distinct\n            |> List.sortBy (fun (severity, section, reason, _, _) -> blockerSeverityRank severity, reportSectionRank section, reason)\n\n        let blockerLines =\n            blockers\n            |> List.map (fun (severity, section, reason, _, _) -> $\"{severity}|{section}|{reason}\")\n            |> function\n                | [] ->\n                    [\n                        \"Info|blocking-reasons-and-next-actions|No blockers detected.\"\n                    ]\n                | values -> values\n\n        let nextActions =\n            blockers\n            |> List.map (fun (_, _, _, category, command) -> category, command)\n            |> List.distinct\n            |> List.sortBy (fun (category, command) -> actionCategoryRank category, command)\n            |> List.map snd\n            |> function\n                | [] -> [ \"No action required.\" ]\n                | values -> values\n\n        let blockingSection =\n            ReviewModels.createReviewReportSection\n                ReviewModels.ReviewReportSections.BlockingReasonsAndNextActions\n                \"Blocking reasons and next actions\"\n                ProjectionSourceStates.Inferred\n                [\n                    ReviewModels.createProjectionSourceStateMetadata\n                        \"blockers\"\n                        ProjectionSourceStates.Inferred\n                        \"Derived from candidate projection, review context, and deterministic required actions.\"\n                ]\n                [\n                    ReviewModels.createReviewReportEntry \"Blockers\" blockerLines\n                    ReviewModels.createReviewReportEntry \"NextActions\" nextActions\n                ]\n                []\n\n        let report = ReviewModels.ReviewReportResult()\n        report.ReviewReportSchemaVersion <- ReviewModels.ReviewReportSchema.Version\n        report.SectionOrder <- ReviewModels.ReviewReportSections.Ordered\n\n        report.Sections <-\n            [\n                identitySection\n                queueSection\n                validationSection\n                reviewSection\n                workItemSection\n                blockingSection\n            ]\n\n        report\n\n    let private tryGetCurrentPolicySnapshotId (context: HttpContext) (targetBranchId: BranchId) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = Policy.CreateActorProxy targetBranchId graceIds.RepositoryId correlationId\n            let! currentPolicy = actorProxy.GetCurrent correlationId\n\n            return\n                currentPolicy\n                |> Option.bind (fun policy ->\n                    let snapshotId = string policy.PolicySnapshotId\n\n                    if String.IsNullOrWhiteSpace(snapshotId) then\n                        Option.None\n                    else\n                        Option.Some snapshotId)\n        }\n\n    let private enrichCandidateReturnValue (context: HttpContext) (parameters: #ReviewParameters) (returnValue: GraceReturnValue<'T>) =\n        let graceIds = getGraceIds context\n\n        returnValue\n            .enhance(getParametersAsDictionary parameters)\n            .enhance(nameof OwnerId, graceIds.OwnerId)\n            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n            .enhance(\"Command\", context.Items[\"Command\"] :?> string)\n            .enhance (\"Path\", context.Request.Path.Value)\n        |> ignore\n\n        returnValue\n\n    let private enrichCandidateError (context: HttpContext) (parameters: #ReviewParameters) (error: GraceError) =\n        let graceIds = getGraceIds context\n\n        error\n            .enhance(getParametersAsDictionary parameters)\n            .enhance(nameof OwnerId, graceIds.OwnerId)\n            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n            .enhance(\"Command\", context.Items[\"Command\"] :?> string)\n            .enhance (\"Path\", context.Request.Path.Value)\n        |> ignore\n\n        error\n\n    let internal resolveCandidateIdentityProjectionWith\n        (resolvePromotionSet: Guid -> Task<Grace.Types.PromotionSet.PromotionSetDto option>)\n        (parameters: ResolveCandidateIdentityParameters)\n        =\n        task {\n            match! resolveCandidatePromotionSetWith resolvePromotionSet parameters with\n            | Error error -> return Error error\n            | Ok (_, normalizedCandidateId, promotionSet) ->\n                let projectionResult =\n                    ReviewModels.createCandidateIdentityProjectionResult\n                        normalizedCandidateId\n                        promotionSet\n                        parameters.OwnerId\n                        parameters.OrganizationId\n                        parameters.RepositoryId\n\n                return Ok projectionResult\n        }\n\n    let internal resolveCandidateIdentityProjection (context: HttpContext) (parameters: ResolveCandidateIdentityParameters) =\n        resolveCandidateIdentityProjectionWith (resolvePromotionSetById context) parameters\n\n    let processCommand<'T when 'T :> ReviewParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<ReviewCommand>) =\n        task {\n            let commandName = context.Items[\"Command\"] :?> string\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let handleCommand promotionSetId cmd =\n                    task {\n                        let actorProxy = Review.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n                        let metadata = createMetadata context\n\n                        match! actorProxy.Handle cmd metadata with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof PromotionSetId, promotionSetId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof PromotionSetId, promotionSetId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result400BadRequest graceError\n                    }\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let! cmd = command parameters\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                    return! handleCommand promotionSetId cmd\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = ReviewError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in Review.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> ReviewParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (query: QueryResult<IReviewActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n                    let actorProxy = Review.CreateActorProxy promotionSetId graceIds.RepositoryId correlationId\n                    let! queryResult = query context 0 actorProxy\n\n                    let graceReturnValue =\n                        (GraceReturnValue.Create queryResult correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof PromotionSetId, promotionSetId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = ReviewError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let private createCandidateActionResult\n        (identity: CandidateIdentityProjection)\n        (actionName: string)\n        (appliedOperations: string list)\n        (diagnostics: string list)\n        =\n        let result = CandidateActionResult()\n        result.Identity <- identity\n        result.Action <- actionName\n        result.AppliedOperations <- appliedOperations\n        result.Diagnostics <- diagnostics\n\n        result.SourceStates <-\n            [\n                ReviewModels.createProjectionSourceStateMetadata \"identity\" ProjectionSourceStates.Authoritative \"Resolved from candidate identity projection.\"\n                ReviewModels.createProjectionSourceStateMetadata\n                    \"operation\"\n                    ProjectionSourceStates.Authoritative\n                    \"Mapped to PromotionSet and Queue backend operations.\"\n            ]\n\n        result\n\n    let private executeCandidateRetry (context: HttpContext) (identity: CandidateIdentityProjection) (promotionSet: PromotionSetDto) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let metadata = createMetadata context\n\n            let promotionSetActorProxy = PromotionSet.CreateActorProxy promotionSet.PromotionSetId graceIds.RepositoryId correlationId\n\n            match! promotionSetActorProxy.Handle (PromotionSetCommand.RecomputeStepsIfStale(Option.Some \"candidate retry\")) metadata with\n            | Error error -> return Error error\n            | Ok _ ->\n                let queueActorProxy = PromotionQueue.CreateActorProxy promotionSet.TargetBranchId graceIds.RepositoryId correlationId\n                let mutable appliedOperations = [ \"PromotionSet.RecomputeStepsIfStale\" ]\n\n                let! queueExists = queueActorProxy.Exists correlationId\n\n                if not queueExists then\n                    let policyActorProxy = Policy.CreateActorProxy promotionSet.TargetBranchId graceIds.RepositoryId correlationId\n                    let! policySnapshot = policyActorProxy.GetCurrent correlationId\n\n                    match policySnapshot with\n                    | Option.Some snapshot ->\n                        let snapshotId = string snapshot.PolicySnapshotId\n\n                        if String.IsNullOrWhiteSpace(snapshotId) then\n                            return\n                                Error(\n                                    GraceError.Create \"Candidate retry requires queue initialization, but policy snapshot context is unavailable.\" correlationId\n                                )\n                        else\n                            match! queueActorProxy.Handle (PromotionQueueCommand.Initialize(promotionSet.TargetBranchId, snapshot.PolicySnapshotId)) metadata\n                                with\n                            | Error error -> return Error error\n                            | Ok _ ->\n                                appliedOperations <- appliedOperations @ [ \"Queue.Initialize\" ]\n\n                                match! queueActorProxy.Handle (PromotionQueueCommand.Enqueue promotionSet.PromotionSetId) metadata with\n                                | Ok _ ->\n                                    appliedOperations <- appliedOperations @ [ \"Queue.Enqueue\" ]\n                                    return Ok(createCandidateActionResult identity \"retry\" appliedOperations [])\n                                | Error error -> return Error error\n                    | Option.None ->\n                        return\n                            Error(\n                                GraceError.Create \"Candidate retry requires queue initialization, but no policy snapshot is currently available.\" correlationId\n                            )\n                else\n                    match! queueActorProxy.Handle (PromotionQueueCommand.Enqueue promotionSet.PromotionSetId) metadata with\n                    | Ok _ ->\n                        appliedOperations <- appliedOperations @ [ \"Queue.Enqueue\" ]\n                        return Ok(createCandidateActionResult identity \"retry\" appliedOperations [])\n                    | Error error -> return Error error\n        }\n\n    let private executeCandidateCancel (context: HttpContext) (identity: CandidateIdentityProjection) (promotionSet: PromotionSetDto) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let metadata = createMetadata context\n            let queueActorProxy = PromotionQueue.CreateActorProxy promotionSet.TargetBranchId graceIds.RepositoryId correlationId\n            let! queueExists = queueActorProxy.Exists correlationId\n\n            if not queueExists then\n                return Error(GraceError.Create \"Candidate cancel is not supported because the target branch queue is not initialized.\" correlationId)\n            else\n                match! queueActorProxy.Handle (PromotionQueueCommand.Dequeue promotionSet.PromotionSetId) metadata with\n                | Ok _ -> return Ok(createCandidateActionResult identity \"cancel\" [ \"Queue.Dequeue\" ] [])\n                | Error error when error.Error = QueueError.getErrorMessage QueueError.PromotionSetNotInQueue ->\n                    return Error(GraceError.Create \"Candidate cancel is not supported because this candidate is not currently queued.\" correlationId)\n                | Error error -> return Error error\n        }\n\n    let private executeCandidateGateRerun (context: HttpContext) (identity: CandidateIdentityProjection) (promotionSet: PromotionSetDto) (gateName: string) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let metadata = createMetadata context\n            let promotionSetActorProxy = PromotionSet.CreateActorProxy promotionSet.PromotionSetId graceIds.RepositoryId correlationId\n\n            let gateReason = $\"candidate gate rerun ({gateName.Trim()})\"\n\n            match! promotionSetActorProxy.Handle (PromotionSetCommand.RecomputeStepsIfStale(Option.Some gateReason)) metadata with\n            | Ok _ ->\n                return\n                    Ok(\n                        createCandidateActionResult\n                            identity\n                            \"gate-rerun\"\n                            [ \"PromotionSet.RecomputeStepsIfStale\" ]\n                            [\n                                \"Gate-specific validation trigger is unavailable; requested deterministic promotion-set recompute instead.\"\n                            ]\n                    )\n            | Error error -> return Error error\n        }\n\n    /// Resolves candidate identity mapping to the underlying PromotionSet identity.\n    let ResolveCandidateIdentity: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters =\n                        context\n                        |> parse<ResolveCandidateIdentityParameters>\n\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"ResolveCandidateIdentity\"\n\n                    match! resolveCandidateIdentityProjection context parameters with\n                    | Ok projection ->\n                        let returnValue =\n                            GraceReturnValue.Create projection correlationId\n                            |> enrichCandidateReturnValue context parameters\n\n                        return! context |> result200Ok returnValue\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (ResolveCandidateIdentityParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Gets candidate projection details from PromotionSet, Queue, and Review sources.\n    let GetCandidate: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateProjectionParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"GetCandidate\"\n\n                    match! resolveCandidateProjectionContext context parameters with\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                    | Ok (identity, promotionSet) ->\n                        let! queue = tryGetQueueForPromotionSet context promotionSet\n                        let! reviewNotes, _ = getReviewStateForPromotionSet context promotionSet.PromotionSetId\n                        let projection = buildCandidateProjectionSnapshot identity promotionSet queue reviewNotes\n\n                        let returnValue =\n                            GraceReturnValue.Create projection correlationId\n                            |> enrichCandidateReturnValue context parameters\n\n                        return! context |> result200Ok returnValue\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateProjectionParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Gets deterministic required actions for a candidate.\n    let GetCandidateRequiredActions: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateProjectionParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"GetCandidateRequiredActions\"\n\n                    match! resolveCandidateProjectionContext context parameters with\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                    | Ok (identity, promotionSet) ->\n                        let! queue = tryGetQueueForPromotionSet context promotionSet\n                        let! reviewNotes, _ = getReviewStateForPromotionSet context promotionSet.PromotionSetId\n                        let snapshot = buildCandidateProjectionSnapshot identity promotionSet queue reviewNotes\n\n                        let requiredActions = CandidateRequiredActionsResult()\n                        requiredActions.Identity <- snapshot.Identity\n                        requiredActions.RequiredActions <- snapshot.RequiredActions\n                        requiredActions.Diagnostics <- snapshot.Diagnostics\n                        requiredActions.SourceStates <- snapshot.SourceStates\n\n                        let returnValue =\n                            GraceReturnValue.Create requiredActions correlationId\n                            |> enrichCandidateReturnValue context parameters\n\n                        return! context |> result200Ok returnValue\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateProjectionParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Gets candidate attestation state from policy and review checkpoint contexts.\n    let GetCandidateAttestations: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateProjectionParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"GetCandidateAttestations\"\n\n                    match! resolveCandidateProjectionContext context parameters with\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                    | Ok (identity, promotionSet) ->\n                        let! reviewNotes, checkpoints = getReviewStateForPromotionSet context promotionSet.PromotionSetId\n\n                        let policySnapshotFromNotes =\n                            reviewNotes\n                            |> Option.map (fun notes -> string notes.PolicySnapshotId)\n                            |> Option.filter (fun snapshotId -> not <| String.IsNullOrWhiteSpace snapshotId)\n\n                        let! policySnapshotFromActor = tryGetCurrentPolicySnapshotId context promotionSet.TargetBranchId\n\n                        let policySnapshotId =\n                            policySnapshotFromNotes\n                            |> Option.orElse policySnapshotFromActor\n\n                        let latestCheckpoint =\n                            checkpoints\n                            |> List.sortByDescending (fun checkpoint -> checkpoint.Timestamp)\n                            |> List.tryHead\n\n                        let attestations, diagnostics, sourceStates = buildCandidateAttestationEntries policySnapshotId latestCheckpoint\n\n                        let result = CandidateAttestationsResult()\n                        result.Identity <- identity\n                        result.Attestations <- attestations\n                        result.Diagnostics <- diagnostics\n                        result.SourceStates <- sourceStates\n\n                        let returnValue =\n                            GraceReturnValue.Create result correlationId\n                            |> enrichCandidateReturnValue context parameters\n\n                        return! context |> result200Ok returnValue\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateProjectionParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Gets a unified review report for candidate-first reviewer workflows.\n    let GetReviewReport: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateProjectionParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"GetReviewReport\"\n\n                    match! resolveCandidateProjectionContext context parameters with\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                    | Ok (identity, promotionSet) ->\n                        let! queue = tryGetQueueForPromotionSet context promotionSet\n                        let! reviewNotes, checkpoints = getReviewStateForPromotionSet context promotionSet.PromotionSetId\n                        let snapshot = buildCandidateProjectionSnapshot identity promotionSet queue reviewNotes\n                        let report = buildReviewReport identity promotionSet snapshot reviewNotes checkpoints\n\n                        let returnValue =\n                            GraceReturnValue.Create report correlationId\n                            |> enrichCandidateReturnValue context parameters\n\n                        return! context |> result200Ok returnValue\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateProjectionParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Retries a candidate by recomputing the PromotionSet and re-enqueueing queue work.\n    let RetryCandidate: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateProjectionParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"RetryCandidate\"\n\n                    match! resolveCandidateProjectionContext context parameters with\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                    | Ok (identity, promotionSet) ->\n                        match! executeCandidateRetry context identity promotionSet with\n                        | Ok actionResult ->\n                            let returnValue =\n                                GraceReturnValue.Create actionResult correlationId\n                                |> enrichCandidateReturnValue context parameters\n\n                            return! context |> result200Ok returnValue\n                        | Error error ->\n                            let candidateError = enrichCandidateError context parameters error\n                            return! context |> result400BadRequest candidateError\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateProjectionParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Cancels queued candidate processing through queue dequeue semantics.\n    let CancelCandidate: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateProjectionParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"CancelCandidate\"\n\n                    match! resolveCandidateProjectionContext context parameters with\n                    | Error error ->\n                        let candidateError = enrichCandidateError context parameters error\n                        return! context |> result400BadRequest candidateError\n                    | Ok (identity, promotionSet) ->\n                        match! executeCandidateCancel context identity promotionSet with\n                        | Ok actionResult ->\n                            let returnValue =\n                                GraceReturnValue.Create actionResult correlationId\n                                |> enrichCandidateReturnValue context parameters\n\n                            return! context |> result200Ok returnValue\n                        | Error error ->\n                            let candidateError = enrichCandidateError context parameters error\n                            return! context |> result400BadRequest candidateError\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateProjectionParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Reruns a candidate gate through deterministic backend recomputation semantics.\n    let RerunCandidateGate: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context |> parse<CandidateGateRerunParameters>\n                    parameters.OwnerId <- graceIds.OwnerIdString\n                    parameters.OrganizationId <- graceIds.OrganizationIdString\n                    parameters.RepositoryId <- graceIds.RepositoryIdString\n                    context.Items[ \"Command\" ] <- \"RerunCandidateGate\"\n\n                    if String.IsNullOrWhiteSpace parameters.Gate then\n                        let candidateError =\n                            GraceError.Create \"Gate is required for candidate gate rerun.\" correlationId\n                            |> enrichCandidateError context parameters\n\n                        return! context |> result400BadRequest candidateError\n                    else\n                        match! resolveCandidateProjectionContext context parameters with\n                        | Error error ->\n                            let candidateError = enrichCandidateError context parameters error\n                            return! context |> result400BadRequest candidateError\n                        | Ok (identity, promotionSet) ->\n                            match! executeCandidateGateRerun context identity promotionSet parameters.Gate with\n                            | Ok actionResult ->\n                                let returnValue =\n                                    GraceReturnValue.Create actionResult correlationId\n                                    |> enrichCandidateReturnValue context parameters\n\n                                return! context |> result200Ok returnValue\n                            | Error error ->\n                                let candidateError = enrichCandidateError context parameters error\n                                return! context |> result400BadRequest candidateError\n                with\n                | ex ->\n                    let candidateError =\n                        GraceError.CreateWithException ex String.Empty correlationId\n                        |> enrichCandidateError context (CandidateGateRerunParameters())\n\n                    return! context |> result500ServerError candidateError\n            }\n\n    /// Gets review notes.\n    let GetNotes: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: GetReviewNotesParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId ReviewError.InvalidPromotionSetId\n                    |]\n\n                let query (context: HttpContext) _ (actorProxy: IReviewActor) = actorProxy.GetNotes(getCorrelationId context)\n\n                let! parameters = context |> parse<GetReviewNotesParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                context.Items[ \"Command\" ] <- \"GetNotes\"\n                return! processQuery context parameters validations query\n            }\n\n    /// Records a review checkpoint.\n    let Checkpoint: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let validations (parameters: ReviewCheckpointParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId ReviewError.InvalidPromotionSetId\n                        Guid.isValidAndNotEmptyGuid parameters.ReviewedUpToReferenceId ReviewError.InvalidReferenceId\n                        String.isNotEmpty parameters.PolicySnapshotId ReviewError.InvalidPolicySnapshotId\n                    |]\n\n                let command (parameters: ReviewCheckpointParameters) =\n                    let promotionSetId = Guid.Parse(parameters.PromotionSetId)\n\n                    let principal =\n                        if\n                            isNull context.User\n                            || isNull context.User.Identity\n                            || String.IsNullOrEmpty(context.User.Identity.Name)\n                        then\n                            Constants.GraceSystemUser\n                        else\n                            context.User.Identity.Name\n\n                    let checkpoint =\n                        {\n                            ReviewCheckpointId = Guid.NewGuid()\n                            PromotionSetId = Option.Some promotionSetId\n                            ReviewedUpToReferenceId = Guid.Parse(parameters.ReviewedUpToReferenceId)\n                            PolicySnapshotId = PolicySnapshotId parameters.PolicySnapshotId\n                            Reviewer = UserId principal\n                            Timestamp = getCurrentInstant ()\n                        }\n\n                    ReviewCommand.AddCheckpoint checkpoint\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof Checkpoint\n                return! processCommand context validations command\n            }\n\n    /// Resolves a finding.\n    let ResolveFinding: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: ResolveFindingParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId ReviewError.InvalidPromotionSetId\n                        Guid.isValidAndNotEmptyGuid parameters.FindingId ReviewError.InvalidFindingId\n                        DiscriminatedUnion.isMemberOf<FindingResolutionState, ReviewError> parameters.ResolutionState ReviewError.InvalidResolutionState\n                    |]\n\n                let command (parameters: ResolveFindingParameters) =\n                    let principal =\n                        if\n                            isNull context.User\n                            || isNull context.User.Identity\n                            || String.IsNullOrEmpty(context.User.Identity.Name)\n                        then\n                            Constants.GraceSystemUser\n                        else\n                            context.User.Identity.Name\n\n                    let resolutionState =\n                        discriminatedUnionFromString<FindingResolutionState> parameters.ResolutionState\n                        |> Option.get\n\n                    let note =\n                        if String.IsNullOrEmpty(parameters.Note) then\n                            Option.None\n                        else\n                            Option.Some parameters.Note\n\n                    ReviewCommand.ResolveFinding(Guid.Parse(parameters.FindingId), resolutionState, UserId principal, note)\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof ResolveFinding\n                return! processCommand context validations command\n            }\n\n    /// Requests deeper analysis (placeholder).\n    let Deepen: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceError = GraceError.Create \"Deepen is not implemented yet.\" correlationId\n                return! context |> result400BadRequest graceError\n            }\n"
  },
  {
    "path": "src/Grace.Server/ReviewAnalysis.Server.fs",
    "content": "namespace Grace.Server\n\nopen Grace.Server.ApplicationContext\nopen Grace.Shared.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen Microsoft.Extensions.Caching.Memory\nopen System\nopen System.Security.Cryptography\nopen System.Text\nopen System.Threading.Tasks\n\nmodule ReviewAnalysis =\n    type CachedTriage = { Result: ReviewModels.TriageResult; Receipt: AnalysisReceipt }\n\n    let triagePromptTemplateVersion = \"v1\"\n    let deepPromptTemplateVersion = \"v1\"\n    let private moreContextMarker = \"NEED_MORE_CONTEXT\"\n\n    let private hashString (value: string) =\n        let bytes = Encoding.UTF8.GetBytes(value)\n        let hashBytes = SHA256.HashData(bytes)\n        Sha256Hash(byteArrayToString hashBytes)\n\n    let private computeEvidenceHash (riskProfile: DeterministicRiskProfile) (evidenceSummary: EvidenceSetSummary) =\n        hashString (serialize (riskProfile, evidenceSummary))\n\n    let private buildCacheKey (policySnapshotId: PolicySnapshotId) (evidenceHash: Sha256Hash) (modelId: string) (promptVersion: string) =\n        $\"{policySnapshotId}|{evidenceHash}|{modelId}|{promptVersion}\"\n\n    let private tryGetCachedTriage (cache: IMemoryCache) (cacheKey: string) =\n        let mutable cached: obj = null\n\n        if cache.TryGetValue(cacheKey, &cached) then\n            cached :?> CachedTriage |> Some\n        else\n            None\n\n    let private storeCachedTriage (cache: IMemoryCache) (cacheKey: string) (cached: CachedTriage) =\n        cache.Set(cacheKey, cached, MemoryCacheEntryOptions(AbsoluteExpirationRelativeToNow = Nullable(TimeSpan.FromHours(12.0))))\n        |> ignore\n\n    let runTriage\n        (provider: ReviewModels.IReviewModelProvider)\n        (policySnapshot: PolicySnapshot)\n        (riskProfile: DeterministicRiskProfile)\n        (evidence: EvidenceSet)\n        (evidenceSummary: EvidenceSetSummary)\n        (triggerReasons: string list)\n        (principal: UserId)\n        =\n        task {\n            let cache = memoryCache\n            let evidenceHash = computeEvidenceHash riskProfile evidenceSummary\n            let cacheKey = buildCacheKey policySnapshot.PolicySnapshotId evidenceHash provider.TriageModelId triagePromptTemplateVersion\n\n            match tryGetCachedTriage cache cacheKey with\n            | Some cached -> return cached.Result, cached.Receipt\n            | None ->\n                let! triageResult = provider.RunTriage { PolicySnapshot = policySnapshot; RiskProfile = riskProfile; Evidence = evidence }\n\n                let outputHash = hashString (serialize triageResult)\n\n                let receipt =\n                    {\n                        AnalysisReceiptId = Guid.NewGuid()\n                        Stage = EvidenceStage.Triage\n                        PolicySnapshotId = policySnapshot.PolicySnapshotId\n                        EvidenceHash = evidenceHash\n                        EvidenceSummary = evidenceSummary\n                        ModelId = provider.TriageModelId\n                        MaxTokens = evidence.Budget.MaxTokens\n                        OutputHash = outputHash\n                        TriggerReasons = triggerReasons\n                        CreatedAt = getCurrentInstant ()\n                        Principal = principal\n                    }\n\n                storeCachedTriage cache cacheKey { Result = triageResult; Receipt = receipt }\n\n                return triageResult, receipt\n        }\n\n    type CachedDeepReview = { Notes: ReviewNotes; Receipts: AnalysisReceipt list }\n\n    let private requiresMoreContext (notes: ReviewNotes) =\n        not (String.IsNullOrWhiteSpace notes.Summary)\n        && notes.Summary.IndexOf(moreContextMarker, StringComparison.OrdinalIgnoreCase)\n           >= 0\n\n    let runDeepReview\n        (provider: ReviewModels.IReviewModelProvider)\n        (policySnapshot: PolicySnapshot)\n        (riskProfile: DeterministicRiskProfile)\n        (evidence: EvidenceSet)\n        (evidenceSummary: EvidenceSetSummary)\n        (triggerReasons: string list)\n        (principal: UserId)\n        (progressiveEvidence: (unit -> EvidenceSet * EvidenceSetSummary) option)\n        =\n        task {\n            let cache = memoryCache\n            let initialEvidenceHash = computeEvidenceHash riskProfile evidenceSummary\n            let cacheKey = buildCacheKey policySnapshot.PolicySnapshotId initialEvidenceHash provider.DeepModelId deepPromptTemplateVersion\n\n            let mutable cached: obj = null\n\n            if cache.TryGetValue(cacheKey, &cached) then\n                let cachedDeep = cached :?> CachedDeepReview\n                return cachedDeep.Notes, cachedDeep.Receipts\n            else\n                let! deepPacket =\n                    provider.RunDeepReview { PolicySnapshot = policySnapshot; RiskProfile = riskProfile; Evidence = evidence; WorkItemContext = None }\n\n                let outputHash = hashString (serialize deepPacket)\n\n                let receipt =\n                    {\n                        AnalysisReceiptId = Guid.NewGuid()\n                        Stage = EvidenceStage.Deep\n                        PolicySnapshotId = policySnapshot.PolicySnapshotId\n                        EvidenceHash = initialEvidenceHash\n                        EvidenceSummary = evidenceSummary\n                        ModelId = provider.DeepModelId\n                        MaxTokens = evidence.Budget.MaxTokens\n                        OutputHash = outputHash\n                        TriggerReasons = triggerReasons\n                        CreatedAt = getCurrentInstant ()\n                        Principal = principal\n                    }\n\n                match progressiveEvidence with\n                | Some getMoreEvidence when requiresMoreContext deepPacket ->\n                    let additionalEvidence, additionalSummary = getMoreEvidence ()\n\n                    let! refinedPacket =\n                        provider.RunDeepReview\n                            { PolicySnapshot = policySnapshot; RiskProfile = riskProfile; Evidence = additionalEvidence; WorkItemContext = None }\n\n                    let refinedHash = hashString (serialize refinedPacket)\n                    let additionalEvidenceHash = computeEvidenceHash riskProfile additionalSummary\n\n                    let followupReceipt =\n                        {\n                            AnalysisReceiptId = Guid.NewGuid()\n                            Stage = EvidenceStage.Deep\n                            PolicySnapshotId = policySnapshot.PolicySnapshotId\n                            EvidenceHash = additionalEvidenceHash\n                            EvidenceSummary = additionalSummary\n                            ModelId = provider.DeepModelId\n                            MaxTokens = additionalEvidence.Budget.MaxTokens\n                            OutputHash = refinedHash\n                            TriggerReasons = triggerReasons\n                            CreatedAt = getCurrentInstant ()\n                            Principal = principal\n                        }\n\n                    cache.Set(cacheKey, { Notes = refinedPacket; Receipts = [ receipt; followupReceipt ] })\n                    |> ignore\n\n                    return refinedPacket, [ receipt; followupReceipt ]\n                | _ ->\n                    cache.Set(cacheKey, { Notes = deepPacket; Receipts = [ receipt ] })\n                    |> ignore\n\n                    return deepPacket, [ receipt ]\n        }\n"
  },
  {
    "path": "src/Grace.Server/ReviewModels.Server.fs",
    "content": "namespace Grace.Server\n\nopen Grace.Shared.Parameters.Review\nopen Grace.Types.Review\nopen Grace.Types.Policy\nopen Microsoft.Extensions.Configuration\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule ReviewModels =\n    type TriageRequest = { PolicySnapshot: PolicySnapshot; RiskProfile: DeterministicRiskProfile; Evidence: EvidenceSet }\n\n    type DeepReviewRequest = { PolicySnapshot: PolicySnapshot; RiskProfile: DeterministicRiskProfile; Evidence: EvidenceSet; WorkItemContext: string option }\n\n    type TriageResult = { Summary: string; RiskLabel: string; Confidence: float; Categories: string list; DeepAnalysisRecommended: bool; Reasons: string list }\n\n    type IReviewModelProvider =\n        abstract member TriageModelId: string\n        abstract member DeepModelId: string\n        abstract member RunTriage: TriageRequest -> Task<TriageResult>\n        abstract member RunDeepReview: DeepReviewRequest -> Task<ReviewNotes>\n\n    type OpenRouterSettings = { ApiBase: string; ApiKeyEnvVar: string; TriageModel: string; DeepModel: string; RequestHeaders: Dictionary<string, string> }\n\n    type ReviewModelsSettings = { Provider: string; OpenRouter: OpenRouterSettings }\n\n    let normalizeCandidateId (candidateId: string) = if isNull candidateId then String.Empty else candidateId.Trim()\n\n    let tryParseCandidateId (candidateId: string) =\n        let normalizedCandidateId = normalizeCandidateId candidateId\n        let isCandidateIdGuid, parsedCandidateId = Guid.TryParse normalizedCandidateId\n\n        if not isCandidateIdGuid\n           || parsedCandidateId = Guid.Empty then\n            Error normalizedCandidateId\n        else\n            let canonicalCandidateId = parsedCandidateId.ToString()\n            Ok(parsedCandidateId, canonicalCandidateId)\n\n    let createCandidateProjectionScope (ownerId: string) (organizationId: string) (repositoryId: string) =\n        let scope = CandidateProjectionScope()\n        scope.OwnerId <- ownerId\n        scope.OrganizationId <- organizationId\n        scope.RepositoryId <- repositoryId\n        scope\n\n    let createProjectionSourceStateMetadata (section: string) (sourceState: string) (detail: string) =\n        let metadata = ProjectionSourceStateMetadata()\n        metadata.Section <- section\n        metadata.SourceState <- sourceState\n        metadata.Detail <- detail\n        metadata\n\n    let createCandidateIdentityProjection\n        (normalizedCandidateId: string)\n        (promotionSetId: Guid)\n        (ownerId: string)\n        (organizationId: string)\n        (repositoryId: string)\n        =\n        let projection = CandidateIdentityProjection()\n        projection.CandidateId <- normalizedCandidateId\n        projection.PromotionSetId <- promotionSetId.ToString()\n        projection.IdentityMode <- CandidateIdentityModes.DirectPromotionSetProjection\n        projection.Scope <- createCandidateProjectionScope ownerId organizationId repositoryId\n        projection\n\n    let createCandidateIdentityProjectionResult\n        (normalizedCandidateId: string)\n        (promotionSet: Grace.Types.PromotionSet.PromotionSetDto)\n        (ownerId: string)\n        (organizationId: string)\n        (repositoryId: string)\n        =\n        let projection = createCandidateIdentityProjection normalizedCandidateId promotionSet.PromotionSetId ownerId organizationId repositoryId\n\n        let sourceState = createProjectionSourceStateMetadata \"identity\" ProjectionSourceStates.Authoritative \"Resolved from PromotionSet.Get.\"\n\n        let result = CandidateIdentityProjectionResult()\n        result.Identity <- projection\n        result.SourceStates <- [ sourceState ]\n        result\n\n    [<RequireQualifiedAccess>]\n    module ReviewReportSchema =\n        [<Literal>]\n        let Version = \"1.0\"\n\n    [<RequireQualifiedAccess>]\n    module ReviewReportSections =\n        [<Literal>]\n        let CandidateAndPromotionSet = \"candidate-and-promotion-set\"\n\n        [<Literal>]\n        let QueueAndRequiredActions = \"queue-and-required-actions\"\n\n        [<Literal>]\n        let ValidationAndGateOutcomes = \"validation-and-gate-outcomes\"\n\n        [<Literal>]\n        let ReviewNotesAndCheckpoint = \"review-notes-and-checkpoint\"\n\n        [<Literal>]\n        let WorkItemLinksAndArtifacts = \"work-item-links-and-artifacts\"\n\n        [<Literal>]\n        let BlockingReasonsAndNextActions = \"blocking-reasons-and-next-actions\"\n\n        let Ordered =\n            [\n                CandidateAndPromotionSet\n                QueueAndRequiredActions\n                ValidationAndGateOutcomes\n                ReviewNotesAndCheckpoint\n                WorkItemLinksAndArtifacts\n                BlockingReasonsAndNextActions\n            ]\n\n    type ReviewReportEntry() =\n        member val public Key = String.Empty with get, set\n        member val public Values: string list = [] with get, set\n\n    type ReviewReportSection() =\n        member val public Section = String.Empty with get, set\n        member val public Title = String.Empty with get, set\n        member val public SourceState = ProjectionSourceStates.NotAvailable with get, set\n        member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n        member val public Entries: ReviewReportEntry list = [] with get, set\n        member val public Diagnostics: string list = [] with get, set\n\n    type ReviewReportResult() =\n        member val public ReviewReportSchemaVersion = ReviewReportSchema.Version with get, set\n        member val public SectionOrder: string list = ReviewReportSections.Ordered with get, set\n        member val public Sections: ReviewReportSection list = [] with get, set\n\n    let createReviewReportEntry (key: string) (values: string list) =\n        let entry = ReviewReportEntry()\n        entry.Key <- key\n        entry.Values <- values\n        entry\n\n    let createReviewReportSection\n        (section: string)\n        (title: string)\n        (sourceState: string)\n        (sourceStates: ProjectionSourceStateMetadata list)\n        (entries: ReviewReportEntry list)\n        (diagnostics: string list)\n        =\n        let reportSection = ReviewReportSection()\n        reportSection.Section <- section\n        reportSection.Title <- title\n        reportSection.SourceState <- sourceState\n        reportSection.SourceStates <- sourceStates\n        reportSection.Entries <- entries\n        reportSection.Diagnostics <- diagnostics\n        reportSection\n\n    type NullReviewModelProvider() =\n        interface IReviewModelProvider with\n            member _.TriageModelId = \"none\"\n            member _.DeepModelId = \"none\"\n\n            member _.RunTriage _ =\n                Task.FromResult(\n                    {\n                        Summary = \"Triage provider not configured.\"\n                        RiskLabel = \"Green\"\n                        Confidence = 0.0\n                        Categories = []\n                        DeepAnalysisRecommended = false\n                        Reasons = [ \"ProviderMissing\" ]\n                    }\n                )\n\n            member _.RunDeepReview _ = Task.FromResult({ ReviewNotes.Default with Summary = \"Deep review provider not configured.\" })\n\n    type OpenRouterReviewModelProvider(settings: OpenRouterSettings) =\n        interface IReviewModelProvider with\n            member _.TriageModelId = settings.TriageModel\n            member _.DeepModelId = settings.DeepModel\n\n            member _.RunTriage _ =\n                Task.FromResult(\n                    {\n                        Summary = $\"OpenRouter triage model configured: {settings.TriageModel}.\"\n                        RiskLabel = \"Yellow\"\n                        Confidence = 0.0\n                        Categories = []\n                        DeepAnalysisRecommended = false\n                        Reasons = [ \"ProviderStub\" ]\n                    }\n                )\n\n            member _.RunDeepReview _ = Task.FromResult({ ReviewNotes.Default with Summary = $\"OpenRouter deep model configured: {settings.DeepModel}.\" })\n\n    let private tryGetSettings (configuration: IConfiguration) =\n        if isNull configuration then\n            None\n        else\n            let section = configuration.GetSection(\"Grace:ReviewModels\")\n\n            if isNull section then\n                None\n            else\n                let settings = section.Get<ReviewModelsSettings>()\n                if isNull (box settings) then None else Some settings\n\n    let createProvider (configuration: IConfiguration) =\n        match tryGetSettings configuration with\n        | Some settings when not <| String.IsNullOrWhiteSpace settings.Provider ->\n            match settings.Provider.Trim() with\n            | \"OpenRouter\" -> OpenRouterReviewModelProvider(settings.OpenRouter) :> IReviewModelProvider\n            | _ -> NullReviewModelProvider() :> IReviewModelProvider\n        | _ -> NullReviewModelProvider() :> IReviewModelProvider\n"
  },
  {
    "path": "src/Grace.Server/Security/AuthorizationMiddleware.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Giraffe\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Threading.Tasks\n\nmodule AuthorizationMiddleware =\n\n    let private log = loggerFactory.CreateLogger(\"AuthorizationMiddleware.Server\")\n\n    let private includeReason = Environment.GetEnvironmentVariable(\"GRACE_TESTING\") = \"1\"\n\n    let private forbidden (reason: string) : HttpHandler =\n        let message =\n            if\n                includeReason\n                && not (String.IsNullOrWhiteSpace reason)\n            then\n                reason\n            else\n                \"Forbidden.\"\n\n        setStatusCode StatusCodes.Status403Forbidden\n        >=> text message\n\n    let private formatResource (resource: Resource) =\n        match resource with\n        | Resource.System -> \"System\"\n        | Resource.Owner ownerId -> $\"Owner:{ownerId}\"\n        | Resource.Organization (ownerId, organizationId) -> $\"Organization:{ownerId}/{organizationId}\"\n        | Resource.Repository (ownerId, organizationId, repositoryId) -> $\"Repository:{ownerId}/{organizationId}/{repositoryId}\"\n        | Resource.Branch (ownerId, organizationId, repositoryId, branchId) -> $\"Branch:{ownerId}/{organizationId}/{repositoryId}/{branchId}\"\n        | Resource.Path (ownerId, organizationId, repositoryId, relativePath) -> $\"Path:{ownerId}/{organizationId}/{repositoryId}:{relativePath}\"\n\n    let private formatResourceSummary (resources: Resource list) =\n        match resources with\n        | [] -> \"None\"\n        | head :: tail ->\n            let headText = formatResource head\n            if tail.IsEmpty then headText else $\"{headText} (+{tail.Length} more)\"\n\n    let private formatPrincipal (principal: Principal) = $\"{principal.PrincipalType}:{principal.PrincipalId}\"\n\n    let private formatPrincipals (principals: Principal list) =\n        match principals with\n        | [] -> \"None\"\n        | _ ->\n            principals\n            |> List.map formatPrincipal\n            |> String.concat \", \"\n\n    let private tryGetIdentityName (context: HttpContext) =\n        let identity = context.User.Identity\n\n        if isNull identity\n           || String.IsNullOrWhiteSpace identity.Name then\n            None\n        else\n            Some identity.Name\n\n    let private formatPrincipalSummary (context: HttpContext) (principals: Principal list) =\n        if principals.IsEmpty then\n            match tryGetIdentityName context with\n            | Some name -> $\"Identity:{name}\"\n            | None -> \"None\"\n        else\n            formatPrincipals principals\n\n    let private logMissingAuthentication (context: HttpContext) (operation: Operation) (principalSummary: string) =\n        if log.IsEnabled(LogLevel.Warning) then\n            log.LogWarning(\n                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Authorization: Authentication required for {operation}. Principal: {principal}. Path: {path}.\",\n                getCurrentInstantExtended (),\n                getMachineName,\n                getCorrelationId context,\n                operation,\n                principalSummary,\n                context.Request.Path.ToString()\n            )\n\n    let private logDenied (context: HttpContext) (operation: Operation) (principalSummary: string) (resourceSummary: string) (reason: string) =\n        if log.IsEnabled(LogLevel.Warning) then\n            log.LogWarning(\n                \"{CurrentInstant}: Node: {HostName}; CorrelationId: {CorrelationId}; Authorization denied for {operation} by {principal} on {resourceSummary}. Reason: {reason}. Path: {path}.\",\n                getCurrentInstantExtended (),\n                getMachineName,\n                getCorrelationId context,\n                operation,\n                principalSummary,\n                resourceSummary,\n                reason,\n                context.Request.Path.ToString()\n            )\n\n    let requiresPermission (operation: Operation) (resourceFromContext: HttpContext -> Task<Resource>) : HttpHandler =\n        fun next context ->\n            task {\n                match PrincipalMapper.tryGetUserId context.User with\n                | None ->\n                    let principalSummary =\n                        PrincipalMapper.getPrincipals context.User\n                        |> formatPrincipalSummary context\n\n                    logMissingAuthentication context operation principalSummary\n                    return! RequestErrors.UNAUTHORIZED \"Grace\" \"Access\" \"Authentication required.\" next context\n                | Some _ ->\n                    let principals = PrincipalMapper.getPrincipals context.User\n                    let principalSummary = formatPrincipals principals\n                    let claims = PrincipalMapper.getEffectiveClaims context.User\n                    let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n                    let! resource = resourceFromContext context\n                    let! decision = evaluator.CheckAsync(principals, claims, operation, resource)\n\n                    match decision with\n                    | Allowed _ -> return! next context\n                    | Denied reason ->\n                        logDenied context operation principalSummary (formatResource resource) reason\n                        return! forbidden reason next context\n            }\n\n    let requiresPermissions (operation: Operation) (resourcesFromContext: HttpContext -> Task<Resource list>) : HttpHandler =\n        fun next context ->\n            task {\n                match PrincipalMapper.tryGetUserId context.User with\n                | None ->\n                    let principalSummary =\n                        PrincipalMapper.getPrincipals context.User\n                        |> formatPrincipalSummary context\n\n                    logMissingAuthentication context operation principalSummary\n                    return! RequestErrors.UNAUTHORIZED \"Grace\" \"Access\" \"Authentication required.\" next context\n                | Some _ ->\n                    let principals = PrincipalMapper.getPrincipals context.User\n                    let principalSummary = formatPrincipals principals\n                    let claims = PrincipalMapper.getEffectiveClaims context.User\n                    let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n                    let! resources = resourcesFromContext context\n\n                    let mutable deniedReason: string option = None\n\n                    for resource in resources do\n                        if deniedReason.IsNone then\n                            let! decision = evaluator.CheckAsync(principals, claims, operation, resource)\n\n                            match decision with\n                            | Allowed _ -> ()\n                            | Denied reason -> deniedReason <- Some reason\n\n                    match deniedReason with\n                    | None -> return! next context\n                    | Some reason ->\n                        logDenied context operation principalSummary (formatResourceSummary resources) reason\n                        return! forbidden reason next context\n            }\n\n    let requiresPermissionResolved (resolve: HttpContext -> Task<Result<Operation * Resource, GraceError>>) : HttpHandler =\n        fun next context ->\n            task {\n                match PrincipalMapper.tryGetUserId context.User with\n                | None ->\n                    let principalSummary =\n                        PrincipalMapper.getPrincipals context.User\n                        |> formatPrincipalSummary context\n\n                    logMissingAuthentication context Operation.SystemAdmin principalSummary\n                    return! RequestErrors.UNAUTHORIZED \"Grace\" \"Access\" \"Authentication required.\" next context\n                | Some _ ->\n                    let! resolved = resolve context\n\n                    match resolved with\n                    | Error error -> return! context |> result400BadRequest error\n                    | Ok (operation, resource) ->\n                        let principals = PrincipalMapper.getPrincipals context.User\n                        let principalSummary = formatPrincipals principals\n                        let claims = PrincipalMapper.getEffectiveClaims context.User\n                        let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n                        let! decision = evaluator.CheckAsync(principals, claims, operation, resource)\n\n                        match decision with\n                        | Allowed _ -> return! next context\n                        | Denied reason ->\n                            logDenied context operation principalSummary (formatResource resource) reason\n                            return! forbidden reason next context\n            }\n\n    let requiresPermissionResolvedOptional (resolve: HttpContext -> Task<Result<(Operation * Resource) option, GraceError>>) : HttpHandler =\n        fun next context ->\n            task {\n                match PrincipalMapper.tryGetUserId context.User with\n                | None ->\n                    let principalSummary =\n                        PrincipalMapper.getPrincipals context.User\n                        |> formatPrincipalSummary context\n\n                    logMissingAuthentication context Operation.SystemAdmin principalSummary\n                    return! RequestErrors.UNAUTHORIZED \"Grace\" \"Access\" \"Authentication required.\" next context\n                | Some _ ->\n                    let! resolved = resolve context\n\n                    match resolved with\n                    | Error error -> return! context |> result400BadRequest error\n                    | Ok None -> return! next context\n                    | Ok (Some (operation, resource)) ->\n                        let principals = PrincipalMapper.getPrincipals context.User\n                        let principalSummary = formatPrincipals principals\n                        let claims = PrincipalMapper.getEffectiveClaims context.User\n                        let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n                        let! decision = evaluator.CheckAsync(principals, claims, operation, resource)\n\n                        match decision with\n                        | Allowed _ -> return! next context\n                        | Denied reason ->\n                            logDenied context operation principalSummary (formatResource resource) reason\n                            return! forbidden reason next context\n            }\n"
  },
  {
    "path": "src/Grace.Server/Security/ClaimMapping.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen System\nopen System.Collections.Generic\nopen System.Security.Claims\n\nmodule ClaimMapping =\n\n    let private getClaimValues (principal: ClaimsPrincipal) (claimType: string) =\n        principal.Claims\n        |> Seq.filter (fun claim -> String.Equals(claim.Type, claimType, StringComparison.OrdinalIgnoreCase))\n        |> Seq.map (fun claim -> claim.Value)\n        |> Seq.filter (fun value -> not (String.IsNullOrWhiteSpace value))\n        |> Seq.toList\n\n    let private tryGetClaimValue (principal: ClaimsPrincipal) (claimTypes: string list) =\n        claimTypes\n        |> Seq.tryPick (fun claimType ->\n            principal.Claims\n            |> Seq.tryFind (fun claim -> String.Equals(claim.Type, claimType, StringComparison.OrdinalIgnoreCase))\n            |> Option.map (fun claim -> claim.Value))\n        |> Option.filter (fun value -> not (String.IsNullOrWhiteSpace value))\n\n    let private splitScopes (value: string) =\n        value.Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries)\n        |> Seq.map (fun scopeValue -> scopeValue.Trim())\n        |> Seq.filter (fun scopeValue -> not (String.IsNullOrWhiteSpace scopeValue))\n        |> Seq.toList\n\n    let computeGraceUserId (principal: ClaimsPrincipal) =\n        let existing =\n            principal.Claims\n            |> Seq.tryFind (fun claim -> claim.Type = PrincipalMapper.GraceUserIdClaim)\n            |> Option.map (fun claim -> claim.Value)\n            |> Option.filter (fun value -> not (String.IsNullOrWhiteSpace value))\n\n        match existing with\n        | Some _ -> None\n        | None -> tryGetClaimValue principal [ \"sub\"; ClaimTypes.NameIdentifier ]\n\n    let mapClaims (principal: ClaimsPrincipal) =\n        let claimsToAdd = ResizeArray<Claim>()\n\n        match computeGraceUserId principal with\n        | Some userId -> claimsToAdd.Add(Claim(PrincipalMapper.GraceUserIdClaim, userId))\n        | None -> ()\n\n        let existingGraceClaims = HashSet<string>(StringComparer.OrdinalIgnoreCase)\n        let existingGroupClaims = HashSet<string>(StringComparer.OrdinalIgnoreCase)\n\n        for value in getClaimValues principal PrincipalMapper.GraceClaim do\n            existingGraceClaims.Add(value) |> ignore\n\n        for value in getClaimValues principal PrincipalMapper.GraceGroupIdClaim do\n            existingGroupClaims.Add(value) |> ignore\n\n        let roleClaims =\n            (getClaimValues principal \"roles\")\n            @ (getClaimValues principal ClaimTypes.Role)\n\n        for value in roleClaims do\n            if existingGraceClaims.Add(value) then\n                claimsToAdd.Add(Claim(PrincipalMapper.GraceClaim, value))\n\n        for value in getClaimValues principal \"wids\" do\n            if existingGraceClaims.Add(value) then\n                claimsToAdd.Add(Claim(PrincipalMapper.GraceClaim, value))\n\n        for value in\n            getClaimValues principal \"scp\"\n            |> List.collect splitScopes do\n            if existingGraceClaims.Add(value) then\n                claimsToAdd.Add(Claim(PrincipalMapper.GraceClaim, value))\n\n        for value in\n            getClaimValues principal \"scope\"\n            |> List.collect splitScopes do\n            if existingGraceClaims.Add(value) then\n                claimsToAdd.Add(Claim(PrincipalMapper.GraceClaim, value))\n\n        for value in getClaimValues principal \"permissions\" do\n            if existingGraceClaims.Add(value) then\n                claimsToAdd.Add(Claim(PrincipalMapper.GraceClaim, value))\n\n        for value in getClaimValues principal \"groups\" do\n            if existingGroupClaims.Add(value) then\n                claimsToAdd.Add(Claim(PrincipalMapper.GraceGroupIdClaim, value))\n\n        claimsToAdd |> Seq.toList\n"
  },
  {
    "path": "src/Grace.Server/Security/ClaimsTransformation.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Microsoft.AspNetCore.Authentication\nopen Microsoft.Extensions.Logging\nopen System.Security.Claims\nopen System.Threading.Tasks\n\n/// Adds Grace-specific claims to authenticated principals.\ntype GraceClaimsTransformation(log: ILogger<GraceClaimsTransformation>) =\n\n    interface IClaimsTransformation with\n        member _.TransformAsync(principal: ClaimsPrincipal) =\n            task {\n                if isNull principal then\n                    return principal\n                else\n                    let identity =\n                        principal.Identities\n                        |> Seq.tryFind (fun mappedIdentity -> mappedIdentity.IsAuthenticated)\n                        |> Option.orElseWith (fun () -> principal.Identities |> Seq.tryHead)\n\n                    match identity with\n                    | None -> return principal\n                    | Some targetIdentity ->\n                        let claimsToAdd = ClaimMapping.mapClaims principal\n                        let claimsToAddCount = claimsToAdd |> List.length\n\n                        if claimsToAddCount > 0 then\n                            for claim in claimsToAdd do\n                                targetIdentity.AddClaim(claim)\n\n                            log.LogDebug(\"GraceClaimsTransformation added {ClaimCount} claim(s).\", claimsToAddCount)\n\n                        return principal\n            }\n"
  },
  {
    "path": "src/Grace.Server/Security/EndpointAuthorizationManifest.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Grace.Types.Authorization\n\nmodule EndpointAuthorizationManifest =\n\n    type ResourceKind =\n        | System\n        | Owner\n        | Organization\n        | Repository\n        | Branch\n        | Path\n\n    type EndpointSecurity =\n        | AllowAnonymous\n        | Authenticated\n        | Authorized of Operation * ResourceKind\n\n    type EndpointSecurityDefinition = { Method: string; Path: string; Security: EndpointSecurity }\n\n    let endpoint method_ path security = { Method = method_; Path = path; Security = security }\n\n    let definitions: EndpointSecurityDefinition list =\n        [\n            endpoint \"GET\" \"/\" AllowAnonymous\n            endpoint \"POST\" \"/access/checkPermission\" Authenticated\n            endpoint \"POST\" \"/access/grantRole\" (Authorized(SystemAdmin, System))\n            endpoint \"POST\" \"/access/listPathPermissions\" (Authorized(RepoAdmin, Repository))\n            endpoint \"POST\" \"/access/listRoleAssignments\" (Authorized(SystemAdmin, System))\n            endpoint \"GET\" \"/access/listRoles\" Authenticated\n            endpoint \"POST\" \"/access/removePathPermission\" (Authorized(RepoAdmin, Repository))\n            endpoint \"POST\" \"/access/revokeRole\" (Authorized(SystemAdmin, System))\n            endpoint \"POST\" \"/access/upsertPathPermission\" (Authorized(RepoAdmin, Repository))\n            endpoint \"POST\" \"/admin/deleteAllFromCosmosDB\" (Authorized(SystemAdmin, System))\n            endpoint \"POST\" \"/admin/deleteAllRemindersFromCosmosDB\" (Authorized(SystemAdmin, System))\n            endpoint \"GET\" \"/auth/login\" AllowAnonymous\n            endpoint \"GET\" \"/auth/login/%s\" AllowAnonymous\n            endpoint \"GET\" \"/auth/logout\" Authenticated\n            endpoint \"GET\" \"/auth/me\" Authenticated\n            endpoint \"GET\" \"/auth/oidc/config\" AllowAnonymous\n            endpoint \"POST\" \"/auth/token/create\" Authenticated\n            endpoint \"POST\" \"/auth/token/list\" Authenticated\n            endpoint \"POST\" \"/auth/token/revoke\" Authenticated\n            endpoint \"POST\" \"/agent/session/active\" (Authorized(RepoRead, Repository))\n            endpoint \"POST\" \"/agent/session/listActive\" (Authorized(RepoRead, Repository))\n            endpoint \"POST\" \"/agent/session/start\" (Authorized(RepoWrite, Repository))\n            endpoint \"POST\" \"/agent/session/status\" (Authorized(RepoRead, Repository))\n            endpoint \"POST\" \"/agent/session/stop\" (Authorized(RepoWrite, Repository))\n            endpoint \"POST\" \"/branch/assign\" Authenticated\n            endpoint \"POST\" \"/branch/checkpoint\" Authenticated\n            endpoint \"POST\" \"/branch/commit\" (Authorized(BranchWrite, Branch))\n            endpoint \"POST\" \"/branch/create\" Authenticated\n            endpoint \"POST\" \"/branch/createExternal\" Authenticated\n            endpoint \"POST\" \"/branch/delete\" Authenticated\n            endpoint \"POST\" \"/branch/enableAssign\" Authenticated\n            endpoint \"POST\" \"/branch/enableAutoRebase\" Authenticated\n            endpoint \"POST\" \"/branch/enableCheckpoint\" Authenticated\n            endpoint \"POST\" \"/branch/enableCommit\" (Authorized(BranchAdmin, Branch))\n            endpoint \"POST\" \"/branch/enableExternal\" Authenticated\n            endpoint \"POST\" \"/branch/enablePromotion\" Authenticated\n            endpoint \"POST\" \"/branch/enableSave\" Authenticated\n            endpoint \"POST\" \"/branch/enableTag\" Authenticated\n            endpoint \"POST\" \"/branch/get\" (Authorized(BranchRead, Branch))\n            endpoint \"POST\" \"/branch/getCheckpoints\" Authenticated\n            endpoint \"POST\" \"/branch/getCommits\" Authenticated\n            endpoint \"POST\" \"/branch/getDiffsForReferenceType\" Authenticated\n            endpoint \"POST\" \"/branch/getEvents\" Authenticated\n            endpoint \"POST\" \"/branch/getExternals\" Authenticated\n            endpoint \"POST\" \"/branch/getParentBranch\" Authenticated\n            endpoint \"POST\" \"/branch/getPromotions\" Authenticated\n            endpoint \"POST\" \"/branch/getRecursiveSize\" Authenticated\n            endpoint \"POST\" \"/branch/getReference\" Authenticated\n            endpoint \"POST\" \"/branch/getReferences\" Authenticated\n            endpoint \"POST\" \"/branch/getSaves\" Authenticated\n            endpoint \"POST\" \"/branch/getTags\" Authenticated\n            endpoint \"POST\" \"/branch/getVersion\" Authenticated\n            endpoint \"POST\" \"/branch/listContents\" Authenticated\n            endpoint \"POST\" \"/branch/promote\" Authenticated\n            endpoint \"POST\" \"/branch/rebase\" Authenticated\n            endpoint \"POST\" \"/branch/save\" Authenticated\n            endpoint \"POST\" \"/branch/setPromotionMode\" Authenticated\n            endpoint \"POST\" \"/branch/tag\" Authenticated\n            endpoint \"POST\" \"/branch/updateParentBranch\" Authenticated\n            endpoint \"POST\" \"/promotion-set/create\" Authenticated\n            endpoint \"POST\" \"/promotion-set/get\" Authenticated\n            endpoint \"POST\" \"/promotion-set/get-events\" Authenticated\n            endpoint \"POST\" \"/promotion-set/update-input-promotions\" Authenticated\n            endpoint \"POST\" \"/promotion-set/recompute\" Authenticated\n            endpoint \"POST\" \"/promotion-set/apply\" Authenticated\n            endpoint \"POST\" \"/promotion-set/%O/resolve-conflicts\" Authenticated\n            endpoint \"POST\" \"/promotion-set/delete\" Authenticated\n            endpoint \"POST\" \"/validation-set/create\" Authenticated\n            endpoint \"POST\" \"/validation-set/get\" Authenticated\n            endpoint \"POST\" \"/validation-set/update\" Authenticated\n            endpoint \"POST\" \"/validation-set/delete\" Authenticated\n            endpoint \"POST\" \"/validation-result/record\" Authenticated\n            endpoint \"POST\" \"/artifact/create\" Authenticated\n            endpoint \"GET\" \"/artifact/%O/download-uri\" Authenticated\n            endpoint \"POST\" \"/diff/getDiff\" Authenticated\n            endpoint \"POST\" \"/diff/getDiffBySha256Hash\" Authenticated\n            endpoint \"POST\" \"/diff/populate\" Authenticated\n            endpoint \"POST\" \"/directory/create\" Authenticated\n            endpoint \"POST\" \"/directory/get\" Authenticated\n            endpoint \"POST\" \"/directory/getByDirectoryIds\" Authenticated\n            endpoint \"POST\" \"/directory/getBySha256Hash\" Authenticated\n            endpoint \"POST\" \"/directory/getDirectoryVersionsRecursive\" Authenticated\n            endpoint \"POST\" \"/directory/getZipFile\" Authenticated\n            endpoint \"POST\" \"/directory/saveDirectoryVersions\" Authenticated\n            endpoint \"GET\" \"/healthz\" AllowAnonymous\n            endpoint \"POST\" \"/organization/create\" Authenticated\n            endpoint \"POST\" \"/organization/delete\" Authenticated\n            endpoint \"POST\" \"/organization/get\" (Authorized(OrgRead, Organization))\n            endpoint \"POST\" \"/organization/listRepositories\" Authenticated\n            endpoint \"POST\" \"/organization/setDescription\" Authenticated\n            endpoint \"POST\" \"/organization/setName\" (Authorized(OrgAdmin, Organization))\n            endpoint \"POST\" \"/organization/setSearchVisibility\" Authenticated\n            endpoint \"POST\" \"/organization/setType\" Authenticated\n            endpoint \"POST\" \"/organization/undelete\" Authenticated\n            endpoint \"POST\" \"/owner/create\" Authenticated\n            endpoint \"POST\" \"/owner/delete\" Authenticated\n            endpoint \"POST\" \"/owner/get\" (Authorized(OwnerRead, Owner))\n            endpoint \"POST\" \"/owner/listOrganizations\" Authenticated\n            endpoint \"POST\" \"/owner/setDescription\" Authenticated\n            endpoint \"POST\" \"/owner/setName\" (Authorized(OwnerAdmin, Owner))\n            endpoint \"POST\" \"/owner/setSearchVisibility\" Authenticated\n            endpoint \"POST\" \"/owner/setType\" Authenticated\n            endpoint \"POST\" \"/owner/undelete\" Authenticated\n            endpoint \"POST\" \"/policy/acknowledge\" Authenticated\n            endpoint \"POST\" \"/policy/current\" Authenticated\n            endpoint \"POST\" \"/queue/dequeue\" Authenticated\n            endpoint \"POST\" \"/queue/enqueue\" Authenticated\n            endpoint \"POST\" \"/queue/pause\" Authenticated\n            endpoint \"POST\" \"/queue/resume\" Authenticated\n            endpoint \"POST\" \"/queue/status\" Authenticated\n            endpoint \"POST\" \"/reminder/create\" Authenticated\n            endpoint \"POST\" \"/reminder/delete\" Authenticated\n            endpoint \"POST\" \"/reminder/get\" Authenticated\n            endpoint \"POST\" \"/reminder/list\" Authenticated\n            endpoint \"POST\" \"/reminder/reschedule\" Authenticated\n            endpoint \"POST\" \"/reminder/updateTime\" Authenticated\n            endpoint \"POST\" \"/repository/create\" Authenticated\n            endpoint \"POST\" \"/repository/delete\" Authenticated\n            endpoint \"POST\" \"/repository/exists\" Authenticated\n            endpoint \"POST\" \"/repository/get\" (Authorized(RepoRead, Repository))\n            endpoint \"POST\" \"/repository/getBranches\" Authenticated\n            endpoint \"POST\" \"/repository/getBranchesByBranchId\" Authenticated\n            endpoint \"POST\" \"/repository/getReferencesByReferenceId\" Authenticated\n            endpoint \"POST\" \"/repository/isEmpty\" Authenticated\n            endpoint \"POST\" \"/repository/setAllowsLargeFiles\" Authenticated\n            endpoint \"POST\" \"/repository/setAnonymousAccess\" Authenticated\n            endpoint \"POST\" \"/repository/setCheckpointDays\" Authenticated\n            endpoint \"POST\" \"/repository/setConflictResolutionPolicy\" Authenticated\n            endpoint \"POST\" \"/repository/setDefaultServerApiVersion\" Authenticated\n            endpoint \"POST\" \"/repository/setDescription\" Authenticated\n            endpoint \"POST\" \"/repository/setDiffCacheDays\" Authenticated\n            endpoint \"POST\" \"/repository/setDirectoryVersionCacheDays\" Authenticated\n            endpoint \"POST\" \"/repository/setLogicalDeleteDays\" Authenticated\n            endpoint \"POST\" \"/repository/setName\" Authenticated\n            endpoint \"POST\" \"/repository/setRecordSaves\" Authenticated\n            endpoint \"POST\" \"/repository/setSaveDays\" Authenticated\n            endpoint \"POST\" \"/repository/setStatus\" Authenticated\n            endpoint \"POST\" \"/repository/setVisibility\" (Authorized(RepoAdmin, Repository))\n            endpoint \"POST\" \"/repository/undelete\" Authenticated\n            endpoint \"POST\" \"/review/checkpoint\" Authenticated\n            endpoint \"POST\" \"/review/candidate/attestations\" Authenticated\n            endpoint \"POST\" \"/review/candidate/cancel\" Authenticated\n            endpoint \"POST\" \"/review/candidate/gate-rerun\" Authenticated\n            endpoint \"POST\" \"/review/candidate/get\" Authenticated\n            endpoint \"POST\" \"/review/candidate/required-actions\" Authenticated\n            endpoint \"POST\" \"/review/candidate/resolve\" Authenticated\n            endpoint \"POST\" \"/review/candidate/retry\" Authenticated\n            endpoint \"POST\" \"/review/deepen\" Authenticated\n            endpoint \"POST\" \"/review/notes\" Authenticated\n            endpoint \"POST\" \"/review/report/get\" Authenticated\n            endpoint \"POST\" \"/review/resolve\" Authenticated\n            endpoint \"POST\" \"/storage/getDownloadUri\" (Authorized(PathRead, Path))\n            endpoint \"POST\" \"/storage/getUploadMetadataForFiles\" (Authorized(PathWrite, Path))\n            endpoint \"POST\" \"/storage/getUploadUri\" (Authorized(PathWrite, Path))\n            endpoint \"POST\" \"/work/create\" (Authorized(RepoWrite, Repository))\n            endpoint \"POST\" \"/work/add-summary\" Authenticated\n            endpoint \"POST\" \"/work/get\" Authenticated\n            endpoint \"POST\" \"/work/link/artifact\" Authenticated\n            endpoint \"POST\" \"/work/link/promotion-set\" Authenticated\n            endpoint \"POST\" \"/work/link/reference\" Authenticated\n            endpoint \"POST\" \"/work/links/list\" Authenticated\n            endpoint \"POST\" \"/work/attachments/list\" Authenticated\n            endpoint \"POST\" \"/work/attachments/show\" Authenticated\n            endpoint \"POST\" \"/work/attachments/download\" Authenticated\n            endpoint \"POST\" \"/work/links/remove/artifact\" Authenticated\n            endpoint \"POST\" \"/work/links/remove/artifact-type\" Authenticated\n            endpoint \"POST\" \"/work/links/remove/promotion-set\" Authenticated\n            endpoint \"POST\" \"/work/links/remove/reference\" Authenticated\n            endpoint \"POST\" \"/work/update\" Authenticated\n            endpoint \"GET\" \"/metrics\" (Authorized(SystemAdmin, System))\n            endpoint \"GET\" \"/notifications\" Authenticated\n        ]\n"
  },
  {
    "path": "src/Grace.Server/Security/ExternalAuthConfig.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Auth\nopen Microsoft.Extensions.Configuration\nopen System\n\nmodule ExternalAuthConfig =\n    type OidcAuthConfig = { Authority: string; Audience: string }\n\n    let private tryGetConfigValue (configuration: IConfiguration) (name: string) =\n        if isNull configuration then\n            None\n        else\n            let value = configuration[getConfigKey name]\n            if String.IsNullOrWhiteSpace value then None else Some(value.Trim())\n\n    let private normalizeAuthority (authority: string) =\n        let trimmed = authority.Trim()\n\n        if trimmed.EndsWith(\"/\", StringComparison.Ordinal) then\n            trimmed\n        else\n            $\"{trimmed}/\"\n\n    let tryGetOidcConfig (configuration: IConfiguration) =\n        match tryGetConfigValue configuration EnvironmentVariables.GraceAuthOidcAuthority,\n              tryGetConfigValue configuration EnvironmentVariables.GraceAuthOidcAudience\n            with\n        | Some authority, Some audience -> Some { Authority = normalizeAuthority authority; Audience = audience.Trim() }\n        | _ -> None\n\n    let tryGetOidcClientConfig (configuration: IConfiguration) =\n        match tryGetConfigValue configuration EnvironmentVariables.GraceAuthOidcAuthority,\n              tryGetConfigValue configuration EnvironmentVariables.GraceAuthOidcAudience,\n              tryGetConfigValue configuration EnvironmentVariables.GraceAuthOidcCliClientId\n            with\n        | Some authority, Some audience, Some cliClientId ->\n            Some { Authority = normalizeAuthority authority; Audience = audience.Trim(); CliClientId = cliClientId.Trim() }\n        | _ -> None\n\n    let isOidcConfigured (configuration: IConfiguration) = tryGetOidcConfig configuration |> Option.isSome\n"
  },
  {
    "path": "src/Grace.Server/Security/PermissionEvaluator.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Grace.Actors\nopen Grace.Shared\nopen Grace.Shared.Authorization\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen System\nopen System.Threading.Tasks\n\ntype IGracePermissionEvaluator =\n    abstract member CheckAsync:\n        principalSet: Principal list * effectiveClaims: Set<string> * operation: Operation * resource: Resource -> Task<PermissionCheckResult>\n\nmodule PermissionEvaluatorDefaults =\n\n    let getAssignmentsForScope (scope: Scope, correlationId: CorrelationId) =\n        task {\n            let scopeKey = AccessControl.getScopeKey scope\n            let actorProxy = Extensions.ActorProxy.AccessControl.CreateActorProxy scopeKey correlationId\n            return! actorProxy.GetAssignments None correlationId\n        }\n\n    let getPathPermissionsForRepository (repositoryId: RepositoryId, correlationId: CorrelationId) =\n        task {\n            let actorProxy = Extensions.ActorProxy.RepositoryPermission.CreateActorProxy repositoryId correlationId\n            return! actorProxy.GetPathPermissions None correlationId\n        }\n\ntype GracePermissionEvaluator\n    (\n        getAssignmentsForScope: Scope * CorrelationId -> Task<RoleAssignment list>,\n        getPathPermissionsForRepository: RepositoryId * CorrelationId -> Task<PathPermission list>\n    ) =\n\n    new () =\n        GracePermissionEvaluator(\n            PermissionEvaluatorDefaults.getAssignmentsForScope,\n            PermissionEvaluatorDefaults.getPathPermissionsForRepository\n        )\n\n    interface IGracePermissionEvaluator with\n        member _.CheckAsync(principalSet, effectiveClaims, operation, resource) =\n            task {\n                try\n                    let correlationId = generateCorrelationId ()\n                    let roleCatalog = RoleCatalog.getAll ()\n                    let scopes = scopesForResource resource\n\n                    let! assignments =\n                        scopes\n                        |> List.map (fun scope -> getAssignmentsForScope (scope, correlationId))\n                        |> Task.WhenAll\n\n                    let allAssignments = assignments |> Seq.collect id |> Seq.toList\n\n                    let! pathPermissions =\n                        match resource with\n                        | Resource.Path (_, _, repositoryId, _) ->\n                            getPathPermissionsForRepository (repositoryId, correlationId)\n                        | _ -> Task.FromResult List.empty\n\n                    return checkPermission roleCatalog allAssignments pathPermissions principalSet effectiveClaims operation resource\n                with\n                | ex -> return Denied $\"Authorization evaluation failed: {ex.Message}\"\n            }\n"
  },
  {
    "path": "src/Grace.Server/Security/PersonalAccessTokenAuth.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.PersonalAccessToken\nopen Microsoft.AspNetCore.Authentication\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.Options\nopen System\nopen System.Security.Claims\nopen System.Text.Encodings.Web\nopen System.Threading.Tasks\n\nmodule PersonalAccessTokenAuth =\n    [<Literal>]\n    let SchemeName = \"GracePat\"\n\n    type PersonalAccessTokenAuthHandler(options: IOptionsMonitor<AuthenticationSchemeOptions>, loggerFactory: ILoggerFactory, encoder: UrlEncoder) =\n        inherit AuthenticationHandler<AuthenticationSchemeOptions>(options, loggerFactory, encoder)\n\n        let tryGetCorrelationId (context: HttpContext) =\n            match context.Items.TryGetValue(Constants.CorrelationId) with\n            | true, value ->\n                match value with\n                | :? string as correlationId -> correlationId\n                | _ -> String.Empty\n            | _ -> String.Empty\n\n        override this.HandleAuthenticateAsync() =\n            let request = this.Request\n            let httpContext = this.Context\n\n            task {\n                let authorization = request.Headers.Authorization.ToString()\n\n                if String.IsNullOrWhiteSpace authorization then\n                    return AuthenticateResult.NoResult()\n                elif not (authorization.StartsWith(\"Bearer \", StringComparison.OrdinalIgnoreCase)) then\n                    return AuthenticateResult.NoResult()\n                else\n                    let token = authorization.Substring(\"Bearer \".Length).Trim()\n\n                    if String.IsNullOrWhiteSpace token then\n                        return AuthenticateResult.NoResult()\n                    elif not (token.StartsWith(TokenPrefix, StringComparison.Ordinal)) then\n                        return AuthenticateResult.NoResult()\n                    else\n                        match tryParseToken token with\n                        | None -> return AuthenticateResult.Fail(\"Invalid token.\")\n                        | Some (userId, tokenId, secret) ->\n                            let correlationId = tryGetCorrelationId httpContext\n                            let actor = PersonalAccessToken.CreateActorProxy userId correlationId\n                            let! validation = actor.ValidateToken tokenId secret (getCurrentInstant ()) correlationId\n\n                            match validation with\n                            | None -> return AuthenticateResult.Fail(\"Invalid token.\")\n                            | Some result ->\n                                let claims = ResizeArray<Claim>()\n                                claims.Add(Claim(PrincipalMapper.GraceUserIdClaim, result.UserId))\n\n                                result.Claims\n                                |> List.iter (fun claimValue -> claims.Add(Claim(PrincipalMapper.GraceClaim, claimValue)))\n\n                                result.GroupIds\n                                |> List.iter (fun groupId -> claims.Add(Claim(PrincipalMapper.GraceGroupIdClaim, groupId)))\n\n                                let identity = ClaimsIdentity(claims, SchemeName)\n                                let principal = ClaimsPrincipal(identity)\n                                let ticket = AuthenticationTicket(principal, SchemeName)\n                                return AuthenticateResult.Success(ticket)\n            }\n"
  },
  {
    "path": "src/Grace.Server/Security/PrincipalMapper.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Grace.Types.Authorization\nopen System\nopen System.Security.Claims\n\nmodule PrincipalMapper =\n\n    [<Literal>]\n    let GraceUserIdClaim = \"grace_user_id\"\n\n    [<Literal>]\n    let GraceGroupIdClaim = \"grace_group_id\"\n\n    [<Literal>]\n    let GraceClaim = \"grace_claim\"\n\n    let tryGetUserId (principal: ClaimsPrincipal) =\n        principal.Claims\n        |> Seq.tryFind (fun claim -> claim.Type = GraceUserIdClaim)\n        |> Option.map (fun claim -> claim.Value)\n        |> Option.filter (fun value -> not (String.IsNullOrWhiteSpace value))\n\n    let private getClaims (principal: ClaimsPrincipal) (claimType: string) =\n        principal.Claims\n        |> Seq.filter (fun claim -> claim.Type = claimType)\n        |> Seq.map (fun claim -> claim.Value)\n        |> Seq.filter (fun value -> not (String.IsNullOrWhiteSpace value))\n        |> Seq.toList\n\n    let getPrincipals (principal: ClaimsPrincipal) =\n        let userId = tryGetUserId principal\n        let groupIds = getClaims principal GraceGroupIdClaim\n\n        let principals =\n            [\n                if userId.IsSome then\n                    { PrincipalType = PrincipalType.User; PrincipalId = userId.Value }\n                for groupId in groupIds do\n                    { PrincipalType = PrincipalType.Group; PrincipalId = groupId }\n            ]\n\n        principals\n\n    let getEffectiveClaims (principal: ClaimsPrincipal) =\n        let claimValues = getClaims principal GraceClaim |> Set.ofList\n\n        let groupClaims =\n            getClaims principal GraceGroupIdClaim\n            |> Set.ofList\n\n        Set.union claimValues groupClaims\n"
  },
  {
    "path": "src/Grace.Server/Security/TestAuth.Server.fs",
    "content": "namespace Grace.Server.Security\n\nopen Microsoft.AspNetCore.Authentication\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.Options\nopen System\nopen System.Security.Claims\nopen System.Text.Encodings.Web\nopen System.Threading.Tasks\n\nmodule TestAuth =\n\n    [<Literal>]\n    let SchemeName = \"GraceTest\"\n\n    [<Literal>]\n    let UserIdHeader = \"x-grace-user-id\"\n\n    [<Literal>]\n    let ClaimsHeader = \"x-grace-claims\"\n\n    [<Literal>]\n    let GroupsHeader = \"x-grace-groups\"\n\n    type GraceTestAuthHandler(options: IOptionsMonitor<AuthenticationSchemeOptions>, loggerFactory: ILoggerFactory, encoder: UrlEncoder) =\n        inherit AuthenticationHandler<AuthenticationSchemeOptions>(options, loggerFactory, encoder)\n\n        override this.HandleAuthenticateAsync() =\n            let request = this.Request\n\n            let tryGetHeader (name: string) =\n                let values = request.Headers[name]\n                if values.Count = 0 then None else Some(values.ToString())\n\n            match tryGetHeader UserIdHeader with\n            | None -> Task.FromResult(AuthenticateResult.NoResult())\n            | Some userId when String.IsNullOrWhiteSpace userId -> Task.FromResult(AuthenticateResult.NoResult())\n            | Some userId ->\n                let claims = ResizeArray<Claim>()\n                claims.Add(Claim(PrincipalMapper.GraceUserIdClaim, userId))\n\n                match tryGetHeader ClaimsHeader with\n                | None -> ()\n                | Some claimHeader when not (String.IsNullOrWhiteSpace claimHeader) ->\n                    claimHeader.Split(';', StringSplitOptions.RemoveEmptyEntries)\n                    |> Array.map (fun claimValue -> claimValue.Trim())\n                    |> Array.filter (fun claimValue -> not (String.IsNullOrWhiteSpace claimValue))\n                    |> Array.iter (fun claimValue -> claims.Add(Claim(PrincipalMapper.GraceClaim, claimValue)))\n                | _ -> ()\n\n                match tryGetHeader GroupsHeader with\n                | None -> ()\n                | Some groupHeader when not (String.IsNullOrWhiteSpace groupHeader) ->\n                    groupHeader.Split(';', StringSplitOptions.RemoveEmptyEntries)\n                    |> Array.map (fun groupValue -> groupValue.Trim())\n                    |> Array.filter (fun groupValue -> not (String.IsNullOrWhiteSpace groupValue))\n                    |> Array.iter (fun groupValue -> claims.Add(Claim(PrincipalMapper.GraceGroupIdClaim, groupValue)))\n                | _ -> ()\n\n                let identity = ClaimsIdentity(claims, SchemeName)\n                let principal = ClaimsPrincipal(identity)\n                let ticket = AuthenticationTicket(principal, SchemeName)\n                Task.FromResult(AuthenticateResult.Success(ticket))\n"
  },
  {
    "path": "src/Grace.Server/Services.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared.Resources.Text\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen Orleans\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Linq\nopen System.Net\nopen System.Threading.Tasks\nopen System.Text\nopen System.Text.Json\n\nmodule Services =\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Server.Services\")\n\n    /// Defines the type of all server queries in Grace.\n    ///\n    /// Takes an HttpContext, the MaxCount of results to return, and the GrainProxy to use for the query, and returns a Task containing the return value.\n    type QueryResult<'T, 'U when 'T :> IGrain> = HttpContext -> int -> 'T -> Task<'U>\n\n    /// Gets the CorrelationId from HttpContext.Items.\n    let getCorrelationId (context: HttpContext) = (context.Items[Constants.CorrelationId] :?> CorrelationId)\n\n    /// Gets the GraceIds record from HttpContext.Items.\n    let getGraceIds (context: HttpContext) =\n        if context.Items.ContainsKey(nameof GraceIds) then\n            context.Items[nameof GraceIds] :?> GraceIds\n        else\n            GraceIds.Default\n\n    /// Creates common metadata for Grace events.\n    let createMetadata (context: HttpContext) : EventMetadata =\n        let metadata =\n            {\n                Timestamp = getCurrentInstant ()\n                CorrelationId =\n                    context\n                        .Items[ Constants.CorrelationId ]\n                        .ToString()\n                Principal = context.User.Identity.Name\n                Properties = new Dictionary<string, string>()\n            }\n\n        let graceIds = getGraceIds context\n\n        if graceIds.RepositoryId <> RepositoryId.Empty then\n            metadata.Properties[ nameof RepositoryId ] <- $\"{graceIds.RepositoryId}\"\n\n        metadata\n\n    /// Parses the incoming request body into the specified type.\n    let parse<'T when 'T :> CommonParameters> (context: HttpContext) =\n        task {\n            let! parameters = context.BindJsonAsync<'T>()\n\n            if String.IsNullOrEmpty(parameters.CorrelationId) then\n                parameters.CorrelationId <- getCorrelationId context\n\n            return parameters\n        }\n\n    /// Parses the incoming request body into the provided type.\n    let deserializeToType (requestBodyType: Type) (context: HttpContext) =\n        task {\n            try\n                let! parameters = JsonSerializer.DeserializeAsync(context.Request.Body, requestBodyType, Constants.JsonSerializerOptions)\n\n                if not <| isNull parameters then return Some parameters else return None\n            with\n            | ex -> return None\n        }\n\n    /// Adds common attributes to the current OpenTelemetry activity, and returns the result.\n    let returnResult<'T> (statusCode: int) (result: 'T) (context: HttpContext) =\n        task {\n            try\n                Activity\n                    .Current\n                    .AddTag(\"correlation_id\", getCorrelationId context)\n                    .AddTag(\"http.status_code\", statusCode)\n                |> ignore\n\n                context.SetStatusCode(statusCode)\n\n                //log.LogDebug(\"{CurrentInstant}: In returnResult: StatusCode: {statusCode}; result: {result}\", getCurrentInstantExtended(), statusCode, serialize result)\n                return! context.WriteJsonAsync(result) // .WriteJsonAsync() uses Grace's JsonSerializerOptions.\n            with\n            | ex -> return! context.WriteJsonAsync(GraceError.CreateWithException ex String.Empty (getCorrelationId context))\n        }\n\n    /// Adds common attributes to the current OpenTelemetry activity, and returns a 404 Not found status.\n    let result404NotFound (context: HttpContext) =\n        task {\n            Activity\n                .Current\n                .AddTag(\"correlation_id\", getCorrelationId context)\n                .AddTag(\"http.status_code\", StatusCodes.Status404NotFound)\n            |> ignore\n\n            context.SetStatusCode(StatusCodes.Status404NotFound)\n            return Some context\n        }\n\n    /// Adds common attributes to the current OpenTelemetry activity, and returns the result with a 200 Ok status.\n    let result200Ok<'T> = returnResult<'T> StatusCodes.Status200OK\n\n    /// Adds common attributes to the current OpenTelemetry activity, and returns the result with a 400 Bad request status.\n    let result400BadRequest<'T> = returnResult<'T> StatusCodes.Status400BadRequest\n\n    // /// Adds common attributes to the current OpenTelemetry activity, and returns the result with a 404 Not found status.\n    // let result404NotFound<'T> = returnResult<'T> StatusCodes.Status404NotFound\n\n    /// Adds common attributes to the current OpenTelemetry activity, and returns the result with a 500 Internal server error status.\n    let result500ServerError<'T> = returnResult<'T> StatusCodes.Status500InternalServerError\n"
  },
  {
    "path": "src/Grace.Server/Startup.Server.fs",
    "content": "namespace Grace.Server\n\nopen Asp.Versioning\nopen Asp.Versioning.ApiExplorer\nopen Azure.Core\nopen Azure.Identity\nopen Azure.Monitor.OpenTelemetry.Exporter\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Models\nopen Giraffe\nopen Giraffe.EndpointRouting\nopen Grace.Actors\nopen Grace.Shared\nopen Grace.Shared.AzureEnvironment\nopen Grace.Shared.Utilities\nopen Grace.Server\nopen Grace.Server.Middleware\nopen Grace.Server.ReminderService\nopen Grace.Server.Security\nopen Grace.Server.Security.TestAuth\nopen Grace.Shared.Converters\nopen Grace.Shared.Parameters\nopen Grace.Types.Automation\nopen Grace.Types.Types\nopen Grace.Types.Authorization\nopen Grace.Types.PersonalAccessToken\nopen Microsoft.AspNetCore.Authentication\nopen Microsoft.AspNetCore.Authentication.JwtBearer\nopen Microsoft.AspNetCore.Authorization\nopen Microsoft.AspNetCore.Builder\nopen Microsoft.AspNetCore.Hosting\nopen Microsoft.AspNetCore.Http\nopen Microsoft.AspNetCore.HttpLogging\nopen Microsoft.AspNetCore.Mvc\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Extensions.Configuration\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Hosting\nopen Microsoft.Extensions.Hosting.Internal\nopen Microsoft.Extensions.Logging\nopen Microsoft.OpenApi\nopen NodaTime\nopen OpenTelemetry\nopen OpenTelemetry.Exporter\nopen OpenTelemetry.Instrumentation.AspNetCore\nopen OpenTelemetry.Metrics\nopen OpenTelemetry.Resources\nopen OpenTelemetry.Trace\nopen Orleans\nopen Orleans.Persistence.Cosmos\nopen Orleans.Serialization\nopen Orleans.Serialization.NodaTime\nopen Swashbuckle.AspNetCore.Swagger\nopen System\nopen System.Linq\nopen System.Reflection\nopen System.Text.Json\nopen System.Collections.Generic\nopen System.Collections.Concurrent\nopen System.Diagnostics\nopen System.IO\nopen System.Linq\nopen System.Text\nopen System.Threading\nopen System.Threading.Tasks\nopen FSharpPlus\nopen System.Net.Http\nopen System.Net.Security\nopen Microsoft.IdentityModel.Tokens\nopen System.Security.Claims\n\nmodule Application =\n\n    type CosmosWarmup(cosmosClient: CosmosClient, log: ILogger<CosmosWarmup>) =\n        interface IHostedService with\n            member _.StartAsync(ct: CancellationToken) : Task =\n                log.LogInformation(\"Waiting for Cosmos DB emulator to be ready...\")\n\n                let rec loop i =\n                    task {\n                        if i > 120 || ct.IsCancellationRequested then\n                            log.LogError(\"Cosmos DB emulator was not ready in time.\")\n                            return ()\n                        else\n                            try\n                                let! _ = cosmosClient.ReadAccountAsync()\n                                log.LogInformation(\"Cosmos DB emulator ready.\")\n                                return ()\n                            with\n                            | ex ->\n                                log.LogWarning(\"Cosmos DB emulator not ready, retry {Try}...\", i)\n                                do! Task.Delay(1000, ct)\n                                return! loop (i + 1)\n                    }\n\n                loop 1 :> Task\n\n            member _.StopAsync(_ct: CancellationToken) : Task = Task.CompletedTask\n\n    let private defaultAzureCredential = lazy (DefaultAzureCredential())\n\n    type Startup(configuration: IConfiguration) =\n\n        do\n            ApplicationContext.setConfiguration configuration\n\n            ApplicationContext.configurePubSubSettings ()\n            |> ApplicationContext.setPubSubSettings\n\n        let notLoggedIn = RequestErrors.UNAUTHORIZED \"Basic\" \"Some Realm\" \"You must be logged in.\"\n\n        let mustBeLoggedIn = requiresAuthentication notLoggedIn\n\n        let graceServerVersion =\n            FileVersionInfo\n                .GetVersionInfo(\n                    Assembly.GetExecutingAssembly().Location\n                )\n                .FileVersion\n\n        let repositoryResourceFromContext (context: HttpContext) =\n            task {\n                let graceIds = Services.getGraceIds context\n                return Resource.Repository(graceIds.OwnerId, graceIds.OrganizationId, graceIds.RepositoryId)\n            }\n\n        let ownerResourceFromContext (context: HttpContext) =\n            task {\n                let graceIds = Services.getGraceIds context\n                return Resource.Owner graceIds.OwnerId\n            }\n\n        let organizationResourceFromContext (context: HttpContext) =\n            task {\n                let graceIds = Services.getGraceIds context\n                return Resource.Organization(graceIds.OwnerId, graceIds.OrganizationId)\n            }\n\n        let branchResourceFromContext (context: HttpContext) =\n            task {\n                let graceIds = Services.getGraceIds context\n                return Resource.Branch(graceIds.OwnerId, graceIds.OrganizationId, graceIds.RepositoryId, graceIds.BranchId)\n            }\n\n        let uploadPathResourcesFromContext (context: HttpContext) =\n            task {\n                context.Request.EnableBuffering()\n                let! parameters = context.BindJsonAsync<Storage.GetUploadMetadataForFilesParameters>()\n\n                context.Request.Body.Seek(0L, IO.SeekOrigin.Begin)\n                |> ignore\n\n                let graceIds = Services.getGraceIds context\n\n                let resources =\n                    parameters.FileVersions\n                    |> Seq.map (fun fileVersion -> Resource.Path(graceIds.OwnerId, graceIds.OrganizationId, graceIds.RepositoryId, fileVersion.RelativePath))\n                    |> Seq.toList\n\n                return resources\n            }\n\n        let uploadUriResourcesFromContext (context: HttpContext) =\n            task {\n                context.Request.EnableBuffering()\n                let! parameters = context.BindJsonAsync<Storage.GetUploadUriParameters>()\n\n                context.Request.Body.Seek(0L, IO.SeekOrigin.Begin)\n                |> ignore\n\n                let graceIds = Services.getGraceIds context\n\n                let resources =\n                    parameters.FileVersions\n                    |> Seq.map (fun fileVersion -> Resource.Path(graceIds.OwnerId, graceIds.OrganizationId, graceIds.RepositoryId, fileVersion.RelativePath))\n                    |> Seq.toList\n\n                return resources\n            }\n\n        let downloadPathResourceFromContext (context: HttpContext) =\n            task {\n                context.Request.EnableBuffering()\n                let! parameters = context.BindJsonAsync<Storage.GetDownloadUriParameters>()\n\n                context.Request.Body.Seek(0L, IO.SeekOrigin.Begin)\n                |> ignore\n\n                let graceIds = Services.getGraceIds context\n\n                return Resource.Path(graceIds.OwnerId, graceIds.OrganizationId, graceIds.RepositoryId, parameters.FileVersion.RelativePath)\n            }\n\n        let composeHandlers (first: HttpHandler) (second: HttpHandler) : HttpHandler = fun next context -> first (second next) context\n\n        let requireSystemAdmin: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.SystemAdmin (fun _ -> task { return Resource.System })\n\n        let requireOwnerAdmin: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.OwnerAdmin ownerResourceFromContext\n\n        let requireOwnerRead: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.OwnerRead ownerResourceFromContext\n\n        let requireOrgAdmin: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.OrgAdmin organizationResourceFromContext\n\n        let requireOrgRead: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.OrgRead organizationResourceFromContext\n\n        let requireRepoAdmin: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.RepoAdmin repositoryResourceFromContext\n\n        let requireRepoRead: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.RepoRead repositoryResourceFromContext\n\n        let requireRepoWrite: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.RepoWrite repositoryResourceFromContext\n\n        let requireBranchAdmin: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.BranchAdmin branchResourceFromContext\n\n        let requireBranchRead: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.BranchRead branchResourceFromContext\n\n        let requireBranchWrite: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.BranchWrite branchResourceFromContext\n\n        let requirePathWrite: HttpHandler = AuthorizationMiddleware.requiresPermissions Operation.PathWrite uploadPathResourcesFromContext\n\n        let requirePathWriteForUploadUri: HttpHandler = AuthorizationMiddleware.requiresPermissions Operation.PathWrite uploadUriResourcesFromContext\n\n        let requirePathRead: HttpHandler = AuthorizationMiddleware.requiresPermission Operation.PathRead downloadPathResourceFromContext\n\n        let activeAgentSessionsByAgentKey = ConcurrentDictionary<string, AgentSessionInfo>()\n        let activeAgentSessionsBySessionId = ConcurrentDictionary<string, string>()\n        let agentSessionOperationCache = ConcurrentDictionary<string, AgentSessionOperationResult>()\n        let bootstrappedAgentKeys = ConcurrentDictionary<string, byte>()\n\n        let tryParseGuid (value: string) =\n            let mutable parsed = Guid.Empty\n\n            if String.IsNullOrWhiteSpace value |> not\n               && Guid.TryParse(value, &parsed)\n               && parsed <> Guid.Empty then\n                Some parsed\n            else\n                Option.None\n\n        let resolveRepositoryId (graceIds: GraceIds) (repositoryIdFromParameters: string) =\n            if graceIds.RepositoryId <> RepositoryId.Empty then\n                graceIds.RepositoryId\n            else\n                repositoryIdFromParameters\n                |> tryParseGuid\n                |> Option.defaultValue RepositoryId.Empty\n\n        let normalizeOperationId (operationId: string) (operationName: string) (correlationId: CorrelationId) =\n            if String.IsNullOrWhiteSpace operationId then\n                $\"{operationName}-{correlationId}\"\n            else\n                operationId.Trim()\n\n        let normalizeSessionSource (source: string) = if String.IsNullOrWhiteSpace source then \"cli\" else source.Trim()\n\n        let toAgentKey (repositoryId: RepositoryId) (agentId: string) =\n            if repositoryId = RepositoryId.Empty\n               || String.IsNullOrWhiteSpace agentId then\n                String.Empty\n            else\n                $\"{repositoryId:N}:{agentId.Trim().ToLowerInvariant()}\"\n\n        let normalizeReplayIdentity (identity: string) =\n            if String.IsNullOrWhiteSpace identity then\n                \"unknown\"\n            else\n                identity.Trim().ToLowerInvariant()\n\n        let toReplayKey (repositoryId: RepositoryId) (identity: string) (operationId: string) =\n            $\"{repositoryId:N}|{normalizeReplayIdentity identity}|{normalizeReplayIdentity operationId}\"\n\n        let setAgentSessionParametersFromGraceIds (graceIds: GraceIds) (parameters: Common.AgentSessionParameters) =\n            if graceIds.OwnerId <> OwnerId.Empty then\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OwnerName <- $\"{graceIds.OwnerName}\"\n\n            if graceIds.OrganizationId <> OrganizationId.Empty then\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.OrganizationName <- $\"{graceIds.OrganizationName}\"\n\n            if graceIds.RepositoryId <> RepositoryId.Empty then\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                parameters.RepositoryName <- $\"{graceIds.RepositoryName}\"\n\n        let createOperationResult (session: AgentSessionInfo) (message: string) (operationId: string) (wasReplay: bool) =\n            { AgentSessionOperationResult.Default with Session = session; Message = message; OperationId = operationId; WasIdempotentReplay = wasReplay }\n\n        let buildAgentSessionMetadata\n            (context: HttpContext)\n            (repositoryId: RepositoryId)\n            (parameters: Common.AgentSessionParameters)\n            (session: AgentSessionInfo)\n            =\n            let metadata = Services.createMetadata context\n\n            if repositoryId <> RepositoryId.Empty then\n                metadata.Properties[ nameof RepositoryId ] <- $\"{repositoryId}\"\n\n            if String.IsNullOrWhiteSpace parameters.OwnerId\n               |> not then\n                metadata.Properties[ nameof OwnerId ] <- parameters.OwnerId\n\n            if String.IsNullOrWhiteSpace parameters.OrganizationId\n               |> not then\n                metadata.Properties[ nameof OrganizationId ] <- parameters.OrganizationId\n\n            if String.IsNullOrWhiteSpace session.AgentId |> not then\n                metadata.Properties[ \"ActorId\" ] <- session.AgentId\n\n            if String.IsNullOrWhiteSpace session.SessionId |> not then\n                metadata.Properties[ \"SessionId\" ] <- session.SessionId\n\n            metadata\n\n        let emitAgentSessionEvent\n            (context: HttpContext)\n            (eventType: AutomationEventType)\n            (metadata: EventMetadata)\n            (operationResult: AgentSessionOperationResult)\n            =\n            task {\n                match EventingPublisher.tryCreateAgentSessionEnvelope eventType metadata operationResult with\n                | Some envelope -> do! Notification.routeAutomationEvent context.RequestServices envelope\n                | None -> ()\n            }\n\n        let tryGetActiveSessionByAgentKey (agentKey: string) =\n            match activeAgentSessionsByAgentKey.TryGetValue(agentKey) with\n            | true, session when session.LifecycleState = AgentSessionLifecycleState.Active -> Some(agentKey, session)\n            | _ -> Option.None\n\n        let tryRemoveSessionByAgentKey (agentKey: string) =\n            let mutable removedSession = AgentSessionInfo.Default\n\n            activeAgentSessionsByAgentKey.TryRemove(agentKey, &removedSession)\n            |> ignore\n\n        let tryRemoveSessionLookup (sessionId: string) =\n            let mutable removedAgentKey = String.Empty\n\n            activeAgentSessionsBySessionId.TryRemove(sessionId, &removedAgentKey)\n            |> ignore\n\n        let tryResolveActiveSession (repositoryId: RepositoryId) (agentId: string) (sessionId: string) (workItemIdOrNumber: string) =\n            let repositoryPrefix = $\"{repositoryId:N}:\"\n\n            if String.IsNullOrWhiteSpace sessionId |> not then\n                match activeAgentSessionsBySessionId.TryGetValue(sessionId) with\n                | true, agentKey when agentKey.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase) -> tryGetActiveSessionByAgentKey agentKey\n                | _ -> Option.None\n            elif String.IsNullOrWhiteSpace agentId |> not then\n                let agentKey = toAgentKey repositoryId agentId\n                tryGetActiveSessionByAgentKey agentKey\n            elif String.IsNullOrWhiteSpace workItemIdOrNumber\n                 |> not then\n                activeAgentSessionsByAgentKey\n                |> Seq.tryPick (fun keyValue ->\n                    if\n                        keyValue.Key.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase)\n                        && keyValue.Value.LifecycleState = AgentSessionLifecycleState.Active\n                        && keyValue.Value.WorkItemIdOrNumber.Equals(workItemIdOrNumber, StringComparison.OrdinalIgnoreCase)\n                    then\n                        Some(keyValue.Key, keyValue.Value)\n                    else\n                        Option.None)\n            else\n                Option.None\n\n        let startAgentSession: HttpHandler =\n            fun (_next: HttpFunc) (context: HttpContext) ->\n                task {\n                    let graceIds = Services.getGraceIds context\n                    let correlationId = Services.getCorrelationId context\n\n                    let! parameters =\n                        context\n                        |> Services.parse<Common.StartAgentSessionParameters>\n\n                    setAgentSessionParametersFromGraceIds graceIds (parameters :> Common.AgentSessionParameters)\n\n                    if String.IsNullOrWhiteSpace parameters.AgentId then\n                        return!\n                            context\n                            |> Services.result400BadRequest (GraceError.Create \"AgentId is required to start an agent session.\" correlationId)\n                    elif String.IsNullOrWhiteSpace parameters.WorkItemIdOrNumber then\n                        return!\n                            context\n                            |> Services.result400BadRequest (GraceError.Create \"WorkItemIdOrNumber is required to start an agent session.\" correlationId)\n                    else\n                        let repositoryId = resolveRepositoryId graceIds parameters.RepositoryId\n\n                        if repositoryId = RepositoryId.Empty then\n                            return!\n                                context\n                                |> Services.result400BadRequest (GraceError.Create \"RepositoryId is required to start an agent session.\" correlationId)\n                        else\n                            let operationId = normalizeOperationId parameters.OperationId \"start\" correlationId\n                            let replayKey = toReplayKey repositoryId parameters.AgentId operationId\n\n                            match agentSessionOperationCache.TryGetValue(replayKey) with\n                            | true, replayResult ->\n                                let replay = { replayResult with WasIdempotentReplay = true }\n\n                                return!\n                                    context\n                                    |> Services.result200Ok (GraceReturnValue.Create replay correlationId)\n                            | _ ->\n                                let agentKey = toAgentKey repositoryId parameters.AgentId\n                                let source = normalizeSessionSource parameters.Source\n\n                                match tryGetActiveSessionByAgentKey agentKey with\n                                | Some (_, existingSession) ->\n                                    if existingSession.WorkItemIdOrNumber.Equals(parameters.WorkItemIdOrNumber, StringComparison.OrdinalIgnoreCase) then\n                                        let replayResult =\n                                            createOperationResult existingSession \"Work session is already active for this work item.\" operationId true\n\n                                        agentSessionOperationCache[replayKey] <- replayResult\n\n                                        return!\n                                            context\n                                            |> Services.result200Ok (GraceReturnValue.Create replayResult correlationId)\n                                    else\n                                        let graceError =\n                                            GraceError.Create\n                                                ($\"An active session already exists for work item '{existingSession.WorkItemIdOrNumber}'. Stop it before starting another.\")\n                                                correlationId\n\n                                        return! context |> Services.result400BadRequest graceError\n                                | None ->\n                                    let now = getCurrentInstant ()\n\n                                    let newSession =\n                                        { AgentSessionInfo.Default with\n                                            SessionId =\n                                                (if String.IsNullOrWhiteSpace parameters.OperationId then\n                                                     Guid.NewGuid().ToString(\"N\")\n                                                 else\n                                                     parameters.OperationId.Trim())\n                                            AgentId = parameters.AgentId\n                                            AgentDisplayName = parameters.AgentDisplayName\n                                            WorkItemIdOrNumber = parameters.WorkItemIdOrNumber\n                                            PromotionSetId = parameters.PromotionSetId\n                                            Source = source\n                                            LifecycleState = AgentSessionLifecycleState.Active\n                                            StartedAt = Some now\n                                            LastUpdatedAt = Some now\n                                        }\n\n                                    activeAgentSessionsByAgentKey[agentKey] <- newSession\n                                    activeAgentSessionsBySessionId[newSession.SessionId] <- agentKey\n\n                                    let operationResult = createOperationResult newSession \"Agent work session started.\" operationId false\n\n                                    agentSessionOperationCache[replayKey] <- operationResult\n\n                                    let metadata = buildAgentSessionMetadata context repositoryId (parameters :> Common.AgentSessionParameters) newSession\n\n                                    do! emitAgentSessionEvent context AutomationEventType.AgentWorkStarted metadata operationResult\n\n                                    if bootstrappedAgentKeys.TryAdd(agentKey, 0uy) then\n                                        do! emitAgentSessionEvent context AutomationEventType.AgentBootstrapped metadata operationResult\n\n                                    return!\n                                        context\n                                        |> Services.result200Ok (GraceReturnValue.Create operationResult correlationId)\n                }\n\n        let stopAgentSession: HttpHandler =\n            fun (_next: HttpFunc) (context: HttpContext) ->\n                task {\n                    let graceIds = Services.getGraceIds context\n                    let correlationId = Services.getCorrelationId context\n\n                    let! parameters =\n                        context\n                        |> Services.parse<Common.StopAgentSessionParameters>\n\n                    setAgentSessionParametersFromGraceIds graceIds (parameters :> Common.AgentSessionParameters)\n\n                    let repositoryId = resolveRepositoryId graceIds parameters.RepositoryId\n\n                    if repositoryId = RepositoryId.Empty then\n                        return!\n                            context\n                            |> Services.result400BadRequest (GraceError.Create \"RepositoryId is required to stop an agent session.\" correlationId)\n                    else\n                        let operationId = normalizeOperationId parameters.OperationId \"stop\" correlationId\n\n                        let replayIdentity =\n                            if String.IsNullOrWhiteSpace parameters.AgentId\n                               |> not then\n                                parameters.AgentId\n                            elif String.IsNullOrWhiteSpace parameters.SessionId\n                                 |> not then\n                                parameters.SessionId\n                            elif String.IsNullOrWhiteSpace parameters.WorkItemIdOrNumber\n                                 |> not then\n                                parameters.WorkItemIdOrNumber\n                            else\n                                \"unknown\"\n\n                        let replayKey = toReplayKey repositoryId replayIdentity operationId\n\n                        match agentSessionOperationCache.TryGetValue(replayKey) with\n                        | true, replayResult ->\n                            let replay = { replayResult with WasIdempotentReplay = true }\n\n                            return!\n                                context\n                                |> Services.result200Ok (GraceReturnValue.Create replay correlationId)\n                        | _ ->\n                            match tryResolveActiveSession repositoryId parameters.AgentId parameters.SessionId parameters.WorkItemIdOrNumber with\n                            | Some (agentKey, activeSession) ->\n                                if String.IsNullOrWhiteSpace parameters.WorkItemIdOrNumber\n                                   |> not\n                                   && not\n                                      <| activeSession.WorkItemIdOrNumber.Equals(parameters.WorkItemIdOrNumber, StringComparison.OrdinalIgnoreCase) then\n                                    return!\n                                        context\n                                        |> Services.result400BadRequest (\n                                            GraceError.Create\n                                                ($\"Active session targets work item '{activeSession.WorkItemIdOrNumber}', but stop requested '{parameters.WorkItemIdOrNumber}'.\")\n                                                correlationId\n                                        )\n                                else\n                                    let now = getCurrentInstant ()\n\n                                    let stoppedSession =\n                                        { activeSession with\n                                            LifecycleState = AgentSessionLifecycleState.Stopped\n                                            LastUpdatedAt = Some now\n                                            StoppedAt = Some now\n                                        }\n\n                                    tryRemoveSessionByAgentKey agentKey\n                                    tryRemoveSessionLookup activeSession.SessionId\n\n                                    let stopMessage =\n                                        if String.IsNullOrWhiteSpace parameters.StopReason then\n                                            \"Agent work session stopped.\"\n                                        else\n                                            $\"Agent work session stopped: {parameters.StopReason}\"\n\n                                    let operationResult = createOperationResult stoppedSession stopMessage operationId false\n\n                                    agentSessionOperationCache[replayKey] <- operationResult\n\n                                    let metadata = buildAgentSessionMetadata context repositoryId (parameters :> Common.AgentSessionParameters) stoppedSession\n\n                                    do! emitAgentSessionEvent context AutomationEventType.AgentWorkStopped metadata operationResult\n\n                                    return!\n                                        context\n                                        |> Services.result200Ok (GraceReturnValue.Create operationResult correlationId)\n                            | None ->\n                                let now = getCurrentInstant ()\n\n                                let inactiveSession =\n                                    { AgentSessionInfo.Default with\n                                        SessionId = parameters.SessionId\n                                        AgentId = parameters.AgentId\n                                        AgentDisplayName = parameters.AgentDisplayName\n                                        WorkItemIdOrNumber = parameters.WorkItemIdOrNumber\n                                        Source = \"cli\"\n                                        LifecycleState = AgentSessionLifecycleState.Inactive\n                                        LastUpdatedAt = Some now\n                                        StoppedAt = Some now\n                                    }\n\n                                let operationResult =\n                                    createOperationResult inactiveSession \"No active session matched this request. Nothing to stop.\" operationId true\n\n                                agentSessionOperationCache[replayKey] <- operationResult\n\n                                return!\n                                    context\n                                    |> Services.result200Ok (GraceReturnValue.Create operationResult correlationId)\n                }\n\n        let getAgentSessionStatus: HttpHandler =\n            fun (_next: HttpFunc) (context: HttpContext) ->\n                task {\n                    let graceIds = Services.getGraceIds context\n                    let correlationId = Services.getCorrelationId context\n\n                    let! parameters =\n                        context\n                        |> Services.parse<Common.GetAgentSessionStatusParameters>\n\n                    setAgentSessionParametersFromGraceIds graceIds (parameters :> Common.AgentSessionParameters)\n\n                    let repositoryId = resolveRepositoryId graceIds parameters.RepositoryId\n\n                    if repositoryId = RepositoryId.Empty then\n                        return!\n                            context\n                            |> Services.result400BadRequest (GraceError.Create \"RepositoryId is required to get agent session status.\" correlationId)\n                    else\n                        let now = getCurrentInstant ()\n\n                        let result =\n                            match tryResolveActiveSession repositoryId parameters.AgentId parameters.SessionId parameters.WorkItemIdOrNumber with\n                            | Some (_, activeSession) ->\n                                createOperationResult\n                                    activeSession\n                                    \"Active agent session found.\"\n                                    (normalizeOperationId String.Empty \"status\" correlationId)\n                                    false\n                            | None ->\n                                let inactiveSession =\n                                    { AgentSessionInfo.Default with\n                                        SessionId = parameters.SessionId\n                                        AgentId = parameters.AgentId\n                                        AgentDisplayName = parameters.AgentDisplayName\n                                        WorkItemIdOrNumber = parameters.WorkItemIdOrNumber\n                                        LifecycleState = AgentSessionLifecycleState.Inactive\n                                        LastUpdatedAt = Some now\n                                    }\n\n                                createOperationResult\n                                    inactiveSession\n                                    \"No active agent session matched this request.\"\n                                    (normalizeOperationId String.Empty \"status\" correlationId)\n                                    false\n\n                        return!\n                            context\n                            |> Services.result200Ok (GraceReturnValue.Create result correlationId)\n                }\n\n        let getActiveAgentSession: HttpHandler =\n            fun (_next: HttpFunc) (context: HttpContext) ->\n                task {\n                    let graceIds = Services.getGraceIds context\n                    let correlationId = Services.getCorrelationId context\n\n                    let! parameters =\n                        context\n                        |> Services.parse<Common.GetActiveAgentSessionParameters>\n\n                    setAgentSessionParametersFromGraceIds graceIds (parameters :> Common.AgentSessionParameters)\n\n                    let repositoryId = resolveRepositoryId graceIds parameters.RepositoryId\n\n                    if repositoryId = RepositoryId.Empty then\n                        return!\n                            context\n                            |> Services.result400BadRequest (GraceError.Create \"RepositoryId is required to get the active agent session.\" correlationId)\n                    else\n                        let now = getCurrentInstant ()\n\n                        let result =\n                            match tryResolveActiveSession repositoryId parameters.AgentId String.Empty parameters.WorkItemIdOrNumber with\n                            | Some (_, activeSession) ->\n                                createOperationResult\n                                    activeSession\n                                    \"Active agent session found.\"\n                                    (normalizeOperationId String.Empty \"active\" correlationId)\n                                    false\n                            | None ->\n                                let inactiveSession =\n                                    { AgentSessionInfo.Default with\n                                        AgentId = parameters.AgentId\n                                        AgentDisplayName = parameters.AgentDisplayName\n                                        WorkItemIdOrNumber = parameters.WorkItemIdOrNumber\n                                        LifecycleState = AgentSessionLifecycleState.Inactive\n                                        LastUpdatedAt = Some now\n                                    }\n\n                                createOperationResult\n                                    inactiveSession\n                                    \"No active agent session matched this request.\"\n                                    (normalizeOperationId String.Empty \"active\" correlationId)\n                                    false\n\n                        return!\n                            context\n                            |> Services.result200Ok (GraceReturnValue.Create result correlationId)\n                }\n\n        let listActiveAgentSessions: HttpHandler =\n            fun (_next: HttpFunc) (context: HttpContext) ->\n                task {\n                    let graceIds = Services.getGraceIds context\n                    let correlationId = Services.getCorrelationId context\n\n                    let! parameters =\n                        context\n                        |> Services.parse<Common.ListActiveAgentSessionsParameters>\n\n                    setAgentSessionParametersFromGraceIds graceIds (parameters :> Common.AgentSessionParameters)\n\n                    let repositoryId = resolveRepositoryId graceIds parameters.RepositoryId\n\n                    if repositoryId = RepositoryId.Empty then\n                        return!\n                            context\n                            |> Services.result400BadRequest (GraceError.Create \"RepositoryId is required to list active agent sessions.\" correlationId)\n                    else\n                        let repositoryPrefix = $\"{repositoryId:N}:\"\n\n                        let maximumSessionCount =\n                            if parameters.MaximumSessionCount <= 0 then\n                                25\n                            else\n                                parameters.MaximumSessionCount\n\n                        let sessions =\n                            activeAgentSessionsByAgentKey\n                            |> Seq.filter (fun keyValue ->\n                                keyValue.Key.StartsWith(repositoryPrefix, StringComparison.OrdinalIgnoreCase)\n                                && keyValue.Value.LifecycleState = AgentSessionLifecycleState.Active)\n                            |> Seq.map (fun keyValue -> keyValue.Value)\n                            |> Seq.sortByDescending (fun session ->\n                                match session.LastUpdatedAt with\n                                | Some instant -> instant.ToUnixTimeTicks()\n                                | None -> Int64.MinValue)\n                            |> Seq.truncate maximumSessionCount\n                            |> Seq.toList\n\n                        let result =\n                            { AgentSessionListResult.Default with\n                                Sessions = sessions\n                                Count = sessions.Length\n                                Message = $\"Found {sessions.Length} active agent session(s).\"\n                            }\n\n                        return!\n                            context\n                            |> Services.result200Ok (GraceReturnValue.Create result correlationId)\n                }\n\n        let endpoints =\n            [\n                GET [ route\n                          \"/\"\n                          (warbler (fun _ ->\n                              htmlString\n                                  $\"<h1>Hello From Grace Server {graceServerVersion}!</h1><br/><p>The current server time is: {getCurrentInstantExtended ()}.</p>\"))\n                      |> addMetadata (AllowAnonymousAttribute())\n                      route\n                          \"/healthz\"\n                          (warbler (fun _ ->\n                              htmlString $\"<h1>Grace server seems healthy!</h1><br/><p>The current server time is: {getCurrentInstantExtended ()}.</p>\"))\n                      |> addMetadata (AllowAnonymousAttribute()) ]\n                PUT []\n                subRoute\n                    \"/branch\"\n                    [\n                        POST [ route \"/assign\" Branch.Assign\n                               |> addMetadata typeof<Branch.AssignParameters>\n\n                               route \"/checkpoint\" Branch.Checkpoint\n                               |> addMetadata typeof<Branch.CreateReferenceParameters>\n\n                               route \"/commit\" (composeHandlers requireBranchWrite Branch.Commit)\n                               |> addMetadata typeof<Branch.CreateReferenceParameters>\n\n                               route \"/create\" Branch.Create\n                               |> addMetadata typeof<Branch.CreateBranchParameters>\n\n                               route \"/createExternal\" Branch.CreateExternal\n                               |> addMetadata typeof<Branch.CreateReferenceParameters>\n\n                               route \"/delete\" Branch.Delete\n                               |> addMetadata typeof<Branch.DeleteBranchParameters>\n\n                               route \"/enableAssign\" Branch.EnableAssign\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enableAutoRebase\" Branch.EnableAutoRebase\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enableCheckpoint\" Branch.EnableCheckpoint\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enableCommit\" (composeHandlers requireBranchAdmin Branch.EnableCommit)\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enableExternal\" Branch.EnableExternal\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enablePromotion\" Branch.EnablePromotion\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enableSave\" Branch.EnableSave\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/enableTag\" Branch.EnableTag\n                               |> addMetadata typeof<Branch.EnableFeatureParameters>\n\n                               route \"/setPromotionMode\" Branch.SetPromotionMode\n                               |> addMetadata typeof<Branch.SetPromotionModeParameters>\n\n                               route \"/get\" (composeHandlers requireBranchRead Branch.Get)\n                               |> addMetadata typeof<Branch.GetBranchParameters>\n\n                               route \"/getEvents\" Branch.GetEvents\n                               |> addMetadata typeof<Branch.GetBranchParameters>\n\n                               route \"/getExternals\" Branch.GetExternals\n                               |> addMetadata typeof<Branch.GetReferenceParameters>\n\n                               route \"/getCheckpoints\" Branch.GetCheckpoints\n                               |> addMetadata typeof<Branch.GetBranchParameters>\n\n                               route \"/getCommits\" Branch.GetCommits\n                               |> addMetadata typeof<Branch.GetBranchParameters>\n\n                               route \"/getDiffsForReferenceType\" Branch.GetDiffsForReferenceType\n                               |> addMetadata typeof<Branch.GetDiffsForReferenceTypeParameters>\n\n                               route \"/getParentBranch\" Branch.GetParentBranch\n                               |> addMetadata typeof<Branch.GetBranchParameters>\n\n                               route \"/getPromotions\" Branch.GetPromotions\n                               |> addMetadata typeof<Branch.GetReferenceParameters>\n\n                               route \"/getRecursiveSize\" Branch.GetRecursiveSize\n                               |> addMetadata typeof<Branch.ListContentsParameters>\n\n                               route \"/getReference\" Branch.GetReference\n                               |> addMetadata typeof<Branch.GetReferenceParameters>\n\n                               route \"/getReferences\" Branch.GetReferences\n                               |> addMetadata typeof<Branch.GetReferencesParameters>\n\n                               route \"/getSaves\" Branch.GetSaves\n                               |> addMetadata typeof<Branch.GetReferenceParameters>\n\n                               route \"/getTags\" Branch.GetTags\n                               |> addMetadata typeof<Branch.GetReferenceParameters>\n\n                               route \"/getVersion\" Branch.GetVersion\n                               |> addMetadata typeof<Branch.GetBranchVersionParameters>\n\n                               route \"/listContents\" Branch.ListContents\n                               |> addMetadata typeof<Branch.ListContentsParameters>\n\n                               route \"/promote\" Branch.Promote\n                               |> addMetadata typeof<Branch.CreateReferenceParameters>\n\n                               route \"/rebase\" Branch.Rebase\n                               |> addMetadata typeof<Branch.RebaseParameters>\n\n                               route \"/save\" Branch.Save\n                               |> addMetadata typeof<Branch.CreateReferenceParameters>\n\n                               route \"/tag\" Branch.Tag\n                               |> addMetadata typeof<Branch.CreateReferenceParameters>\n\n                               route \"/updateParentBranch\" Branch.UpdateParentBranch\n                               |> addMetadata typeof<Branch.UpdateParentBranchParameters> ]\n                    ]\n                subRoute\n                    \"/diff\"\n                    [\n                        POST [ route \"/getDiff\" Diff.GetDiff\n                               |> addMetadata typeof<Diff.GetDiffParameters>\n\n                               route \"/getDiffBySha256Hash\" Diff.GetDiffBySha256Hash\n                               |> addMetadata typeof<Diff.GetDiffBySha256HashParameters>\n\n                               route \"/populate\" Diff.Populate\n                               |> addMetadata typeof<Diff.PopulateParameters> ]\n                    ]\n                subRoute\n                    \"/directory\"\n                    [\n                        POST [ route \"/create\" DirectoryVersion.Create\n                               |> addMetadata typeof<DirectoryVersion.CreateParameters>\n\n                               route \"/get\" DirectoryVersion.Get\n                               |> addMetadata typeof<DirectoryVersion.GetParameters>\n\n                               route \"/getByDirectoryIds\" DirectoryVersion.GetByDirectoryIds\n                               |> addMetadata typeof<DirectoryVersion.GetByDirectoryIdsParameters>\n\n                               route \"/getBySha256Hash\" DirectoryVersion.GetBySha256Hash\n                               |> addMetadata typeof<DirectoryVersion.GetBySha256HashParameters>\n\n                               route \"/getDirectoryVersionsRecursive\" DirectoryVersion.GetDirectoryVersionsRecursive\n                               |> addMetadata typeof<DirectoryVersion.GetParameters>\n\n                               route \"/getZipFile\" DirectoryVersion.GetZipFile\n                               |> addMetadata typeof<DirectoryVersion.GetZipFileParameters>\n\n                               route \"/saveDirectoryVersions\" DirectoryVersion.SaveDirectoryVersions\n                               |> addMetadata typeof<DirectoryVersion.SaveDirectoryVersionsParameters> ]\n                    ]\n                subRoute \"/notifications\" [ GET [] ]\n                subRoute\n                    \"/organization\"\n                    [\n                        POST [ route \"/create\" Organization.Create\n                               |> addMetadata typeof<Organization.CreateOrganizationParameters>\n\n                               route \"/delete\" Organization.Delete\n                               |> addMetadata typeof<Organization.DeleteOrganizationParameters>\n\n                               route \"/get\" (composeHandlers requireOrgRead Organization.Get)\n                               |> addMetadata typeof<Organization.GetOrganizationParameters>\n\n                               route \"/listRepositories\" Organization.ListRepositories\n                               |> addMetadata typeof<Organization.GetOrganizationParameters>\n\n                               route \"/setDescription\" Organization.SetDescription\n                               |> addMetadata typeof<Organization.SetOrganizationDescriptionParameters>\n\n                               route \"/setName\" (composeHandlers requireOrgAdmin Organization.SetName)\n                               |> addMetadata typeof<Organization.SetOrganizationNameParameters>\n\n                               route \"/setSearchVisibility\" Organization.SetSearchVisibility\n                               |> addMetadata typeof<Organization.SetOrganizationSearchVisibilityParameters>\n\n                               route \"/setType\" Organization.SetType\n                               |> addMetadata typeof<Organization.SetOrganizationTypeParameters>\n\n                               route \"/undelete\" Organization.Undelete\n                               |> addMetadata typeof<Organization.UndeleteOrganizationParameters> ]\n                    ]\n                subRoute\n                    \"/owner\"\n                    [\n                        POST [ route \"/create\" Owner.Create\n                               |> addMetadata typeof<Owner.CreateOwnerParameters>\n\n                               route \"/delete\" Owner.Delete\n                               |> addMetadata typeof<Owner.DeleteOwnerParameters>\n\n                               route \"/get\" (composeHandlers requireOwnerRead Owner.Get)\n                               |> addMetadata typeof<Owner.GetOwnerParameters>\n\n                               route \"/listOrganizations\" Owner.ListOrganizations\n                               |> addMetadata typeof<Owner.GetOwnerParameters>\n\n                               route \"/setDescription\" Owner.SetDescription\n                               |> addMetadata typeof<Owner.SetOwnerDescriptionParameters>\n\n                               route \"/setName\" (composeHandlers requireOwnerAdmin Owner.SetName)\n                               |> addMetadata typeof<Owner.SetOwnerNameParameters>\n\n                               route \"/setSearchVisibility\" Owner.SetSearchVisibility\n                               |> addMetadata typeof<Owner.SetOwnerSearchVisibilityParameters>\n\n                               route \"/setType\" Owner.SetType\n                               |> addMetadata typeof<Owner.SetOwnerTypeParameters>\n\n                               route \"/undelete\" Owner.Undelete\n                               |> addMetadata typeof<Owner.UndeleteOwnerParameters> ]\n                    ]\n                subRoute\n                    \"/agent\"\n                    [\n                        POST [ route \"/session/start\" (composeHandlers requireRepoWrite startAgentSession)\n                               |> addMetadata typeof<Grace.Shared.Parameters.Common.StartAgentSessionParameters>\n\n                               route \"/session/stop\" (composeHandlers requireRepoWrite stopAgentSession)\n                               |> addMetadata typeof<Grace.Shared.Parameters.Common.StopAgentSessionParameters>\n\n                               route \"/session/status\" (composeHandlers requireRepoRead getAgentSessionStatus)\n                               |> addMetadata typeof<Grace.Shared.Parameters.Common.GetAgentSessionStatusParameters>\n\n                               route \"/session/active\" (composeHandlers requireRepoRead getActiveAgentSession)\n                               |> addMetadata typeof<Grace.Shared.Parameters.Common.GetActiveAgentSessionParameters>\n\n                               route \"/session/listActive\" (composeHandlers requireRepoRead listActiveAgentSessions)\n                               |> addMetadata typeof<Grace.Shared.Parameters.Common.ListActiveAgentSessionsParameters> ]\n                    ]\n                subRoute\n                    \"/work\"\n                    [\n                        POST [ route \"/create\" (composeHandlers requireRepoWrite WorkItem.Create)\n                               |> addMetadata typeof<WorkItem.CreateWorkItemParameters>\n\n                               route \"/get\" WorkItem.Get\n                               |> addMetadata typeof<WorkItem.GetWorkItemParameters>\n\n                               route \"/update\" WorkItem.Update\n                               |> addMetadata typeof<WorkItem.UpdateWorkItemParameters>\n\n                               route \"/add-summary\" WorkItem.AddSummary\n                               |> addMetadata typeof<WorkItem.AddSummaryParameters>\n\n                               route \"/link/reference\" WorkItem.LinkReference\n                               |> addMetadata typeof<WorkItem.LinkReferenceParameters>\n\n                               route \"/link/artifact\" WorkItem.LinkArtifact\n                               |> addMetadata typeof<WorkItem.LinkArtifactParameters>\n\n                               route \"/link/promotion-set\" WorkItem.LinkPromotionSet\n                               |> addMetadata typeof<WorkItem.LinkPromotionSetParameters>\n\n                               route \"/links/list\" WorkItem.GetLinks\n                               |> addMetadata typeof<WorkItem.GetWorkItemLinksParameters>\n\n                               route \"/attachments/list\" WorkItem.ListAttachments\n                               |> addMetadata typeof<WorkItem.ListWorkItemAttachmentsParameters>\n\n                               route \"/attachments/show\" WorkItem.ShowAttachment\n                               |> addMetadata typeof<WorkItem.ShowWorkItemAttachmentParameters>\n\n                               route \"/attachments/download\" WorkItem.DownloadAttachment\n                               |> addMetadata typeof<WorkItem.DownloadWorkItemAttachmentParameters>\n\n                               route \"/links/remove/reference\" WorkItem.RemoveReferenceLink\n                               |> addMetadata typeof<WorkItem.RemoveReferenceLinkParameters>\n\n                               route \"/links/remove/promotion-set\" WorkItem.RemovePromotionSetLink\n                               |> addMetadata typeof<WorkItem.RemovePromotionSetLinkParameters>\n\n                               route \"/links/remove/artifact\" WorkItem.RemoveArtifactLink\n                               |> addMetadata typeof<WorkItem.RemoveArtifactLinkParameters>\n\n                               route \"/links/remove/artifact-type\" WorkItem.RemoveArtifactTypeLinks\n                               |> addMetadata typeof<WorkItem.RemoveArtifactTypeLinksParameters> ]\n                    ]\n                subRoute\n                    \"/policy\"\n                    [\n                        POST [ route \"/current\" Policy.GetCurrent\n                               |> addMetadata typeof<Policy.GetPolicyParameters>\n\n                               route \"/acknowledge\" Policy.Acknowledge\n                               |> addMetadata typeof<Policy.AcknowledgePolicyParameters> ]\n                    ]\n                subRoute\n                    \"/review\"\n                    [\n                        POST [ route \"/notes\" Review.GetNotes\n                               |> addMetadata typeof<Review.GetReviewNotesParameters>\n\n                               route \"/candidate/resolve\" Review.ResolveCandidateIdentity\n                               |> addMetadata typeof<Review.ResolveCandidateIdentityParameters>\n\n                               route \"/candidate/get\" Review.GetCandidate\n                               |> addMetadata typeof<Review.CandidateProjectionParameters>\n\n                               route \"/candidate/required-actions\" Review.GetCandidateRequiredActions\n                               |> addMetadata typeof<Review.CandidateProjectionParameters>\n\n                               route \"/candidate/attestations\" Review.GetCandidateAttestations\n                               |> addMetadata typeof<Review.CandidateProjectionParameters>\n\n                               route \"/report/get\" Review.GetReviewReport\n                               |> addMetadata typeof<Review.CandidateProjectionParameters>\n\n                               route \"/candidate/retry\" Review.RetryCandidate\n                               |> addMetadata typeof<Review.CandidateProjectionParameters>\n\n                               route \"/candidate/cancel\" Review.CancelCandidate\n                               |> addMetadata typeof<Review.CandidateProjectionParameters>\n\n                               route \"/candidate/gate-rerun\" Review.RerunCandidateGate\n                               |> addMetadata typeof<Review.CandidateGateRerunParameters>\n\n                               route \"/checkpoint\" Review.Checkpoint\n                               |> addMetadata typeof<Review.ReviewCheckpointParameters>\n\n                               route \"/resolve\" Review.ResolveFinding\n                               |> addMetadata typeof<Review.ResolveFindingParameters>\n\n                               route \"/deepen\" Review.Deepen\n                               |> addMetadata typeof<Review.DeepenReviewParameters> ]\n                    ]\n                subRoute\n                    \"/queue\"\n                    [\n                        POST [ route \"/status\" Queue.Status\n                               |> addMetadata typeof<Queue.QueueStatusParameters>\n\n                               route \"/enqueue\" Queue.Enqueue\n                               |> addMetadata typeof<Queue.EnqueueParameters>\n\n                               route \"/pause\" Queue.Pause\n                               |> addMetadata typeof<Queue.QueueActionParameters>\n\n                               route \"/resume\" Queue.Resume\n                               |> addMetadata typeof<Queue.QueueActionParameters>\n\n                               route \"/dequeue\" Queue.Dequeue\n                               |> addMetadata typeof<Queue.PromotionSetActionParameters> ]\n                    ]\n                subRoute\n                    \"/promotion-set\"\n                    [\n                        POST [ route \"/create\" PromotionSet.Create\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.CreatePromotionSetParameters>\n\n                               route \"/get\" PromotionSet.Get\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.GetPromotionSetParameters>\n\n                               route \"/get-events\" PromotionSet.GetEvents\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.GetPromotionSetEventsParameters>\n\n                               route \"/update-input-promotions\" PromotionSet.UpdateInputPromotions\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.UpdatePromotionSetInputPromotionsParameters>\n\n                               route \"/recompute\" PromotionSet.Recompute\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.RecomputePromotionSetParameters>\n\n                               route \"/apply\" PromotionSet.Apply\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.ApplyPromotionSetParameters>\n\n                               routef \"/%O/resolve-conflicts\" PromotionSet.ResolveConflicts\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.ResolvePromotionSetConflictsParameters>\n\n                               route \"/delete\" PromotionSet.Delete\n                               |> addMetadata typeof<Grace.Shared.Parameters.PromotionSet.DeletePromotionSetParameters> ]\n                    ]\n                subRoute\n                    \"/validation-set\"\n                    [\n                        POST [ route \"/create\" ValidationSet.Create\n                               |> addMetadata typeof<Grace.Shared.Parameters.Validation.CreateValidationSetParameters>\n\n                               route \"/get\" ValidationSet.Get\n                               |> addMetadata typeof<Grace.Shared.Parameters.Validation.GetValidationSetParameters>\n\n                               route \"/update\" ValidationSet.Update\n                               |> addMetadata typeof<Grace.Shared.Parameters.Validation.UpdateValidationSetParameters>\n\n                               route \"/delete\" ValidationSet.Delete\n                               |> addMetadata typeof<Grace.Shared.Parameters.Validation.DeleteValidationSetParameters> ]\n                    ]\n                subRoute\n                    \"/validation-result\"\n                    [\n                        POST [ route \"/record\" ValidationResult.Record\n                               |> addMetadata typeof<Grace.Shared.Parameters.Validation.RecordValidationResultParameters> ]\n                    ]\n                subRoute\n                    \"/artifact\"\n                    [\n                        POST [ route \"/create\" Artifact.Create\n                               |> addMetadata typeof<Grace.Shared.Parameters.Artifact.CreateArtifactParameters> ]\n\n                        GET [ routef \"/%O/download-uri\" Artifact.GetDownloadUri ]\n                    ]\n                subRoute\n                    \"/repository\"\n                    [\n                        POST [ route \"/create\" Repository.Create\n                               |> addMetadata typeof<Repository.CreateRepositoryParameters>\n\n                               route \"/delete\" (composeHandlers requireRepoAdmin Repository.Delete)\n                               |> addMetadata typeof<Repository.DeleteRepositoryParameters>\n\n                               route \"/exists\" Repository.Exists\n                               |> addMetadata typeof<Repository.RepositoryParameters>\n\n                               route \"/get\" (composeHandlers requireRepoRead Repository.Get)\n                               |> addMetadata typeof<Repository.RepositoryParameters>\n\n                               route \"/getBranches\" Repository.GetBranches\n                               |> addMetadata typeof<Repository.GetBranchesParameters>\n\n                               route \"/getBranchesByBranchId\" Repository.GetBranchesByBranchId\n                               |> addMetadata typeof<Repository.GetBranchesByBranchIdParameters>\n\n                               route \"/getReferencesByReferenceId\" Repository.GetReferencesByReferenceId\n                               |> addMetadata typeof<Repository.GetReferencesByReferenceIdParameters>\n\n                               route \"/isEmpty\" Repository.IsEmpty\n                               |> addMetadata typeof<Repository.IsEmptyParameters>\n\n                               route \"/setAllowsLargeFiles\" Repository.SetAllowsLargeFiles\n                               |> addMetadata typeof<Repository.SetAllowsLargeFilesParameters>\n\n                               route \"/setAnonymousAccess\" Repository.SetAnonymousAccess\n                               |> addMetadata typeof<Repository.SetAnonymousAccessParameters>\n\n                               route \"/setCheckpointDays\" Repository.SetCheckpointDays\n                               |> addMetadata typeof<Repository.SetCheckpointDaysParameters>\n\n                               route \"/setDiffCacheDays\" Repository.SetDiffCacheDays\n                               |> addMetadata typeof<Repository.SetDiffCacheDaysParameters>\n\n                               route \"/setDirectoryVersionCacheDays\" Repository.SetDirectoryVersionCacheDays\n                               |> addMetadata typeof<Repository.SetDirectoryVersionCacheDaysParameters>\n\n                               route \"/setDefaultServerApiVersion\" Repository.SetDefaultServerApiVersion\n                               |> addMetadata typeof<Repository.SetDefaultServerApiVersionParameters>\n\n                               route \"/setConflictResolutionPolicy\" Repository.SetConflictResolutionPolicy\n                               |> addMetadata typeof<Repository.SetConflictResolutionPolicyParameters>\n\n                               route \"/setDescription\" (composeHandlers requireRepoAdmin Repository.SetDescription)\n                               |> addMetadata typeof<Repository.SetRepositoryDescriptionParameters>\n\n                               route \"/setLogicalDeleteDays\" Repository.SetLogicalDeleteDays\n                               |> addMetadata typeof<Repository.SetLogicalDeleteDaysParameters>\n\n                               route \"/setName\" Repository.SetName\n                               |> addMetadata typeof<Repository.SetRepositoryNameParameters>\n\n                               route \"/setRecordSaves\" Repository.SetRecordSaves\n                               |> addMetadata typeof<Repository.RecordSavesParameters>\n\n                               route \"/setSaveDays\" Repository.SetSaveDays\n                               |> addMetadata typeof<Repository.SetSaveDaysParameters>\n\n                               route \"/setStatus\" Repository.SetStatus\n                               |> addMetadata typeof<Repository.SetRepositoryStatusParameters>\n\n                               route \"/setVisibility\" (composeHandlers requireRepoAdmin Repository.SetVisibility)\n                               |> addMetadata typeof<Repository.SetRepositoryVisibilityParameters>\n\n                               route \"/undelete\" Repository.Undelete\n                               |> addMetadata typeof<Repository.UndeleteRepositoryParameters> ]\n                    ]\n                subRoute\n                    \"/storage\"\n                    [\n                        POST [ route \"/getUploadMetadataForFiles\" (composeHandlers requirePathWrite Storage.GetUploadMetadataForFiles)\n                               |> addMetadata typeof<Storage.GetUploadMetadataForFilesParameters>\n\n                               route \"/getDownloadUri\" (composeHandlers requirePathRead Storage.GetDownloadUri)\n                               |> addMetadata typeof<Storage.GetDownloadUriParameters>\n\n                               route \"/getUploadUri\" (composeHandlers requirePathWriteForUploadUri Storage.GetUploadUris)\n                               |> addMetadata typeof<Storage.GetUploadUriParameters> ]\n                    ]\n                subRoute\n                    \"/access\"\n                    [\n                        POST [ route \"/grantRole\" Access.GrantRole\n                               |> addMetadata typeof<Access.GrantRoleParameters>\n\n                               route \"/revokeRole\" Access.RevokeRole\n                               |> addMetadata typeof<Access.RevokeRoleParameters>\n\n                               route \"/listRoleAssignments\" Access.ListRoleAssignments\n                               |> addMetadata typeof<Access.ListRoleAssignmentsParameters>\n\n                               route \"/upsertPathPermission\" Access.UpsertPathPermission\n                               |> addMetadata typeof<Access.UpsertPathPermissionParameters>\n\n                               route \"/removePathPermission\" Access.RemovePathPermission\n                               |> addMetadata typeof<Access.RemovePathPermissionParameters>\n\n                               route \"/listPathPermissions\" Access.ListPathPermissions\n                               |> addMetadata typeof<Access.ListPathPermissionsParameters>\n\n                               route \"/checkPermission\" Access.CheckPermission\n                               |> addMetadata typeof<Access.CheckPermissionParameters> ]\n                        GET [ route \"/listRoles\" Access.ListRoles ]\n                    ]\n                subRoute\n                    \"/auth\"\n                    [\n                        GET [ route \"/me\" Auth.Me\n                              route \"/oidc/config\" (Auth.OidcConfig configuration)\n                              |> addMetadata (AllowAnonymousAttribute())\n                              route \"/login\" Auth.Login\n                              |> addMetadata (AllowAnonymousAttribute())\n                              routef \"/login/%s\" (fun providerId -> Auth.LoginProvider providerId)\n                              |> addMetadata (AllowAnonymousAttribute())\n                              route \"/logout\" Auth.Logout ]\n                        POST [ route \"/token/create\" (Auth.TokenCreate configuration)\n                               |> addMetadata typeof<Auth.CreatePersonalAccessTokenParameters>\n                               route \"/token/list\" (Auth.TokenList configuration)\n                               |> addMetadata typeof<Auth.ListPersonalAccessTokensParameters>\n                               route \"/token/revoke\" (Auth.TokenRevoke configuration)\n                               |> addMetadata typeof<Auth.RevokePersonalAccessTokenParameters> ]\n                    ]\n                subRoute\n                    \"/reminder\"\n                    [\n                        POST [ route \"/list\" Reminder.List\n                               |> addMetadata typeof<Reminder.ListRemindersParameters>\n\n                               route \"/get\" Reminder.Get\n                               |> addMetadata typeof<Reminder.GetReminderParameters>\n\n                               route \"/delete\" Reminder.Delete\n                               |> addMetadata typeof<Reminder.DeleteReminderParameters>\n\n                               route \"/updateTime\" Reminder.UpdateTime\n                               |> addMetadata typeof<Reminder.UpdateReminderTimeParameters>\n\n                               route \"/reschedule\" Reminder.Reschedule\n                               |> addMetadata typeof<Reminder.RescheduleReminderParameters>\n\n                               route \"/create\" Reminder.Create\n                               |> addMetadata typeof<Reminder.CreateReminderParameters> ]\n                    ]\n                subRoute\n                    \"/admin\"\n                    [\n                        POST [\n#if DEBUG\n                               route \"/deleteAllFromCosmosDB\" (composeHandlers requireSystemAdmin Storage.DeleteAllFromCosmosDB)\n                               route \"/deleteAllRemindersFromCosmosDB\" (composeHandlers requireSystemAdmin Storage.DeleteAllRemindersFromCosmosDB)\n#endif\n                                ]\n                    ]\n            ]\n\n        let notFoundHandler = \"Not Found\" |> text |> RequestErrors.notFound\n\n        let mutable currentWorkingSet = String.Empty\n        let mutable maxWorkingSet = String.Empty\n        let mutable lastMetricsUpdateTime = Instant.MinValue\n        let mutable threadCount = String.Empty\n\n        let enrichTelemetry (activity: Activity) (request: HttpRequest) = //(eventName: string) (obj: Object) =\n            let currentProcess = Process.GetCurrentProcess()\n            let context = request.HttpContext\n\n            if (lastMetricsUpdateTime + Duration.FromSeconds 10.0) < getCurrentInstant () then\n                currentWorkingSet <- currentProcess.WorkingSet64.ToString(\"N0\")\n                maxWorkingSet <- currentProcess.PeakWorkingSet64.ToString(\"N0\")\n                lastMetricsUpdateTime <- getCurrentInstant ()\n                threadCount <- currentProcess.Threads.Count.ToString(\"N0\")\n\n            let user = context.User\n\n            if user.Identity.IsAuthenticated then\n                let claimsList = stringBuilderPool.Get()\n\n                try\n                    if not <| isNull user.Claims then\n                        for claim in user.Claims do\n                            claimsList.Append($\"{claim.Type}:{claim.Value};\")\n                            |> ignore\n\n                    if claimsList.Length > 1 then\n                        claimsList.Remove(claimsList.Length - 1, 1)\n                        |> ignore\n\n                    activity\n                        .AddTag(\"enduser.id\", user.Identity.Name)\n                        .AddTag(\"enduser.claims\", claimsList.ToString())\n                    |> ignore\n                finally\n                    stringBuilderPool.Return(claimsList)\n\n            activity\n                .AddTag(\"working_set\", currentWorkingSet)\n                .AddTag(\"max_working_set\", maxWorkingSet)\n                .AddTag(\"thread_count\", threadCount)\n                .AddTag(\"http.client_ip\", context.Connection.RemoteIpAddress)\n                .AddTag(\"enduser.is_authenticated\", user.Identity.IsAuthenticated)\n            |> ignore\n\n        member _.ConfigureServices(services: IServiceCollection) =\n            let mutable configurationObj: obj = null\n\n            if\n                not\n                <| memoryCache.TryGetValue(Constants.MemoryCache.GraceConfiguration, &configurationObj)\n            then\n                invalidOp \"Grace configuration not found in memory cache.\"\n\n            let configuration = configurationObj :?> IConfigurationRoot\n\n            let azureMonitorConnectionString =\n                let connectionString = Environment.GetEnvironmentVariable Constants.EnvironmentVariables.ApplicationInsightsConnectionString\n\n                if String.IsNullOrWhiteSpace connectionString then\n                    configuration[\"Grace:ApplicationInsightsConnectionString\"]\n                else\n                    connectionString\n\n            ApplicationContext.setActorStateStorageProvider ActorStateStorageProvider.AzureCosmosDb\n\n            // OpenTelemetry trace attribute specifications: https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions\n            let globalOpenTelemetryAttributes = Dictionary<string, obj>()\n            globalOpenTelemetryAttributes.Add(\"host.name\", Environment.MachineName)\n            globalOpenTelemetryAttributes.Add(\"process.pid\", Environment.ProcessId)\n\n            globalOpenTelemetryAttributes.Add(\n                \"process.starttime\",\n                Process\n                    .GetCurrentProcess()\n                    .StartTime.ToUniversalTime()\n                    .ToString(\"u\")\n            )\n\n            globalOpenTelemetryAttributes.Add(\"process.executable.name\", Process.GetCurrentProcess().ProcessName)\n            globalOpenTelemetryAttributes.Add(\"process.runtime.version\", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription)\n\n            let openApiInfo = new OpenApiInfo()\n            openApiInfo.Description <- \"Grace is a version control system. Code and documentation can be found at https://gracevcs.com.\"\n            openApiInfo.Title <- \"Grace Server API\"\n            openApiInfo.Version <- \"v0.2\"\n            openApiInfo.Contact <- new OpenApiContact()\n            openApiInfo.Contact.Name <- \"Scott Arbeit\"\n            openApiInfo.Contact.Email <- \"scott.arbeit@outlook.com\"\n            openApiInfo.Contact.Url <- Uri(\"https://gracevcs.com\")\n\n            // Telemetry configuration\n            let graceServerAppId = \"grace-server-integration-test\"\n\n            let tracingOtlpEndpoint = Environment.GetEnvironmentVariable(\"OTLP_ENDPOINT_URL\")\n            let otel = services.AddOpenTelemetry()\n\n            otel\n                .ConfigureResource(fun resourceBuilder ->\n                    resourceBuilder\n                        .AddService(graceServerAppId)\n                        .AddTelemetrySdk()\n                        .AddAttributes(globalOpenTelemetryAttributes)\n                    |> ignore)\n\n                .WithMetrics(fun metricsBuilder ->\n                    metricsBuilder\n                        .AddAspNetCoreInstrumentation()\n                        .AddMeter(\"Microsoft.AspNetCore.Hosting\")\n                        .AddMeter(\"Microsoft.AspNetCore.Server.Kestrel\")\n                        .AddPrometheusExporter(fun prometheusOptions -> prometheusOptions.ScrapeEndpointPath <- \"/metrics\")\n                    |> ignore\n\n                    if\n                        not\n                        <| String.IsNullOrWhiteSpace(azureMonitorConnectionString)\n                    then\n                        logToConsole \"OpenTelemetry: Configuring Azure Monitor metrics exporter\"\n\n                        metricsBuilder.AddAzureMonitorMetricExporter(fun options -> options.ConnectionString <- azureMonitorConnectionString)\n                        |> ignore)\n                .WithTracing(fun traceBuilder ->\n                    traceBuilder\n                        .AddAspNetCoreInstrumentation()\n                        .AddHttpClientInstrumentation()\n                        .AddSource(graceServerAppId)\n                    |> ignore\n\n                    if\n                        not\n                        <| String.IsNullOrWhiteSpace(tracingOtlpEndpoint)\n                    then\n                        logToConsole $\"OpenTelemetry: Configuring OTLP exporter to {tracingOtlpEndpoint}\"\n\n                        traceBuilder.AddOtlpExporter(fun options -> options.Endpoint <- Uri(tracingOtlpEndpoint))\n                        |> ignore\n                    else\n                        traceBuilder.AddConsoleExporter() |> ignore\n\n                    if\n                        not\n                        <| String.IsNullOrWhiteSpace(azureMonitorConnectionString)\n                    then\n                        logToConsole \"OpenTelemetry: Configuring Azure Monitor trace exporter\"\n\n                        traceBuilder.AddAzureMonitorTraceExporter(fun options -> options.ConnectionString <- azureMonitorConnectionString)\n                        |> ignore)\n            |> ignore\n\n            let isTesting =\n                match Environment.GetEnvironmentVariable(\"GRACE_TESTING\") with\n                | null -> false\n                | value ->\n                    value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                    || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n                    || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase)\n\n            if isTesting then\n                services\n                    .AddAuthentication(fun options ->\n                        options.DefaultScheme <- \"GraceAuth\"\n                        options.DefaultChallengeScheme <- \"GraceAuth\")\n                    .AddPolicyScheme(\n                        \"GraceAuth\",\n                        \"GraceAuth\",\n                        fun options ->\n                            options.ForwardDefaultSelector <-\n                                fun context ->\n                                    let authorization = context.Request.Headers.Authorization.ToString()\n\n                                    if\n                                        not (String.IsNullOrWhiteSpace authorization)\n                                        && authorization.StartsWith(\"Bearer \", StringComparison.OrdinalIgnoreCase)\n                                    then\n                                        let token = authorization.Substring(\"Bearer \".Length).Trim()\n\n                                        if token.StartsWith(TokenPrefix, StringComparison.Ordinal) then\n                                            PersonalAccessTokenAuth.SchemeName\n                                        else\n                                            TestAuth.SchemeName\n                                    else\n                                        TestAuth.SchemeName\n                    )\n                    .AddScheme<AuthenticationSchemeOptions, GraceTestAuthHandler>(TestAuth.SchemeName, (fun _ -> ()))\n                    .AddScheme<AuthenticationSchemeOptions, PersonalAccessTokenAuth.PersonalAccessTokenAuthHandler>(\n                        PersonalAccessTokenAuth.SchemeName,\n                        fun _ -> ()\n                    )\n                |> ignore\n            else\n                let oidcConfig = ExternalAuthConfig.tryGetOidcConfig configuration\n                let hasOidc = oidcConfig |> Option.isSome\n\n                let authBuilder =\n                    services.AddAuthentication (fun options ->\n                        options.DefaultScheme <- \"GraceAuth\"\n                        options.DefaultChallengeScheme <- \"GraceAuth\")\n\n                authBuilder\n                    .AddPolicyScheme(\n                        \"GraceAuth\",\n                        \"GraceAuth\",\n                        fun options ->\n                            options.ForwardDefaultSelector <-\n                                fun context ->\n                                    let authorization = context.Request.Headers.Authorization.ToString()\n\n                                    if\n                                        not (String.IsNullOrWhiteSpace authorization)\n                                        && authorization.StartsWith(\"Bearer \", StringComparison.OrdinalIgnoreCase)\n                                    then\n                                        let token = authorization.Substring(\"Bearer \".Length).Trim()\n\n                                        if token.StartsWith(TokenPrefix, StringComparison.Ordinal) then\n                                            PersonalAccessTokenAuth.SchemeName\n                                        else if hasOidc then\n                                            JwtBearerDefaults.AuthenticationScheme\n                                        else\n                                            PersonalAccessTokenAuth.SchemeName\n                                    else if hasOidc then\n                                        JwtBearerDefaults.AuthenticationScheme\n                                    else\n                                        PersonalAccessTokenAuth.SchemeName\n                    )\n                    .AddScheme<AuthenticationSchemeOptions, PersonalAccessTokenAuth.PersonalAccessTokenAuthHandler>(\n                        PersonalAccessTokenAuth.SchemeName,\n                        fun _ -> ()\n                    )\n                |> ignore\n\n                match oidcConfig with\n                | Some config ->\n                    authBuilder.AddJwtBearer(\n                        JwtBearerDefaults.AuthenticationScheme,\n                        fun options ->\n                            options.Authority <- config.Authority\n                            options.Audience <- config.Audience\n\n                            options.TokenValidationParameters <-\n                                TokenValidationParameters(\n                                    ValidateIssuer = true,\n                                    ValidateAudience = true,\n                                    NameClaimType = \"name\",\n                                    RoleClaimType = \"roles\",\n                                    ValidAudience = config.Audience\n                                )\n                    )\n                    |> ignore\n                | None -> ()\n\n            services.AddAuthorization (fun options ->\n                options.FallbackPolicy <-\n                    AuthorizationPolicyBuilder()\n                        .RequireAuthenticatedUser()\n                        .Build())\n            |> ignore\n\n            services.AddTransient<IClaimsTransformation, GraceClaimsTransformation>()\n            |> ignore\n\n            services.AddSingleton<IGracePermissionEvaluator, GracePermissionEvaluator>()\n            |> ignore\n\n            services.AddW3CLogging (fun options ->\n                options.FileName <- \"Grace.Server.log-\"\n\n                let tempPath =\n                    match Environment.GetEnvironmentVariable(\"TEMP\") with\n                    | value when not (String.IsNullOrWhiteSpace value) -> value\n                    | _ -> Path.GetTempPath()\n\n                options.LogDirectory <- Path.Combine(tempPath, \"Grace.Server.Logs\"))\n            |> ignore\n\n            services\n                .AddHostedService<CosmosWarmup>()\n                .AddGiraffe()\n                // Next line adds the Json serializer that Giraffe uses internally.\n                .AddSingleton<Json.ISerializer>(\n                    Json.Serializer(Constants.JsonSerializerOptions)\n                )\n                .AddSingleton<IPartitionKeyProvider, GracePartitionKeyProvider>()\n                .AddRouting()\n                .AddLogging()\n                .AddHostedService<ReminderService>()\n                .AddHostedService<Notification.Subscriber.GraceEventSubscriptionService>()\n                .AddHttpLogging()\n                .AddOrleans(fun siloBuilder ->\n                    siloBuilder.Services.AddSerializer (fun serializerBuilder ->\n                        serializerBuilder.AddNodaTimeSerializers()\n                        |> ignore)\n                    |> ignore)\n            |> ignore\n\n            services.AddSingleton<CosmosClient> (fun serviceProvider ->\n                let cosmosConnectionString = configuration.GetValue<string>(getConfigKey Constants.EnvironmentVariables.AzureCosmosDBConnectionString)\n\n                // Force SNI = \"localhost\" while we connect to 127.0.0.1.\n                let httpHandler =\n                    // Create and configure SslClientAuthenticationOptions\n                    let sslOptions = SslClientAuthenticationOptions()\n                    sslOptions.TargetHost <- \"localhost\" // SNI host_name must be DNS per RFC 6066\n                    sslOptions.RemoteCertificateValidationCallback <- RemoteCertificateValidationCallback(fun _ _ _ _ -> true)\n                    new SocketsHttpHandler(SslOptions = sslOptions)\n\n                let options =\n                    new CosmosClientOptions(\n                        ConnectionMode = ConnectionMode.Gateway,\n                        UseSystemTextJsonSerializerWithOptions = Constants.JsonSerializerOptions,\n                        HttpClientFactory = (fun () -> new HttpClient(httpHandler, disposeHandler = true)),\n                        LimitToEndpoint = true // prevents discovery probes that can trigger TLS issues on emulator\n                    )\n\n                if AzureEnvironment.useManagedIdentity then\n                    let endpoint =\n                        AzureEnvironment.tryGetCosmosEndpointUri ()\n                        |> Option.defaultWith (fun () -> invalidOp \"Azure Cosmos DB endpoint must be configured when using a managed identity.\")\n\n                    new CosmosClient(endpoint.AbsoluteUri, defaultAzureCredential.Value, options)\n                else\n                    if String.IsNullOrWhiteSpace cosmosConnectionString then\n                        invalidOp \"Azure Cosmos DB connection string is required when managed identity is disabled.\"\n\n                    new CosmosClient(cosmosConnectionString, options))\n            |> ignore\n\n            let apiVersioningBuilder =\n                services.AddApiVersioning (fun options ->\n                    options.ReportApiVersions <- true\n                    options.DefaultApiVersion <- new ApiVersion(1, 0)\n                    options.AssumeDefaultVersionWhenUnspecified <- true\n                    // Use whatever reader you want\n                    options.ApiVersionReader <-\n                        ApiVersionReader.Combine(\n                            new UrlSegmentApiVersionReader(),\n                            new HeaderApiVersionReader(\"x-api-version\"),\n                            new MediaTypeApiVersionReader(\"x-api-version\")\n                        ))\n\n            apiVersioningBuilder.AddApiExplorer (fun options ->\n                // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service\n                // note: the specified format code will format the version as \"'v'major[.minor][-status]\"\n                options.GroupNameFormat <- \"'v'VVV\"\n                options.DefaultApiVersion <- ApiVersion(DateOnly(2023, 10, 1))\n                options.AssumeDefaultVersionWhenUnspecified <- true\n                options.ApiVersionParameterSource <- HeaderApiVersionReader(Constants.ServerApiVersionHeaderKey)\n\n                // note: this option is only necessary when versioning by url segment. the SubstitutionFormat\n                // can also be used to control the format of the API version in route templates\n                options.SubstituteApiVersionInUrl <- true)\n            |> ignore\n\n            services\n                .AddSignalR(fun options -> options.EnableDetailedErrors <- true)\n                //.AddStackExchangeRedis(\n                //    \"localhost:6379\",\n                //    fun options ->\n                //        options.Configuration.ChannelPrefix <- StackExchange.Redis.RedisChannel.Literal(\"grace-server\")\n                //        options.Configuration.AbortOnConnectFail <- false\n                //)\n                .AddJsonProtocol(fun options -> options.PayloadSerializerOptions <- Constants.JsonSerializerOptions)\n            |> ignore\n\n            services.AddSingleton<ReviewModels.IReviewModelProvider>(fun _ -> ReviewModels.createProvider configuration)\n            |> ignore\n\n            services.AddSingleton<Grace.Types.PromotionSetConflictModel.IConflictResolutionModelProvider> (fun _ ->\n                Grace.Types.PromotionSetConflictModel.createProvider configuration)\n            |> ignore\n\n            logToConsole $\"Exiting ConfigureServices.\"\n\n        // List all services to the log.\n        //services |> Seq.iter (fun service -> logToConsole $\"Service: {service.ServiceType}.\")\n\n        member _.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =\n            let blobServiceClient = Context.blobServiceClient\n            let containers = blobServiceClient.GetBlobContainers()\n\n            let diffContainerName = Environment.GetEnvironmentVariable Constants.EnvironmentVariables.DiffContainerName\n\n            if not\n               <| containers.Any(fun c -> c.Name = diffContainerName) then\n                logToConsole $\"Creating blob container: {diffContainerName}.\"\n\n                blobServiceClient.CreateBlobContainer(diffContainerName, PublicAccessType.None)\n                |> ignore\n\n            if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore\n\n            app\n                //.UseMiddleware<FakeMiddleware>()\n                .UseMiddleware<HttpSecurityHeadersMiddleware>()\n                .UseW3CLogging()\n                .UseMiddleware<CorrelationIdMiddleware>()\n                .UseMiddleware<LogRequestHeadersMiddleware>()\n                .UseStaticFiles()\n                .UseRouting()\n                .UseAuthentication()\n                .UseMiddleware<LogAuthorizationFailureMiddleware>()\n                .UseAuthorization()\n                .Use(\n                    Func<HttpContext, RequestDelegate, Task> (fun (context: HttpContext) (next: RequestDelegate) ->\n                        task {\n                            if context.Request.Path.StartsWithSegments(PathString(\"/metrics\")) then\n                                let includeReason = Environment.GetEnvironmentVariable(\"GRACE_TESTING\") = \"1\"\n\n                                match PrincipalMapper.tryGetUserId context.User with\n                                | None ->\n                                    context.Response.StatusCode <- StatusCodes.Status401Unauthorized\n                                    do! context.Response.WriteAsync(\"Authentication required.\")\n                                | Some _ ->\n                                    let principals = PrincipalMapper.getPrincipals context.User\n                                    let claims = PrincipalMapper.getEffectiveClaims context.User\n                                    let evaluator = context.RequestServices.GetRequiredService<IGracePermissionEvaluator>()\n                                    let! decision = evaluator.CheckAsync(principals, claims, Operation.SystemAdmin, Resource.System)\n\n                                    match decision with\n                                    | Allowed _ -> return! next.Invoke(context)\n                                    | Denied reason ->\n                                        context.Response.StatusCode <- StatusCodes.Status403Forbidden\n\n                                        let message =\n                                            if\n                                                includeReason\n                                                && not (String.IsNullOrWhiteSpace reason)\n                                            then\n                                                reason\n                                            else\n                                                \"Forbidden.\"\n\n                                        do! context.Response.WriteAsync(message)\n                            else\n                                return! next.Invoke(context)\n                        }\n                        :> Task)\n                )\n                .UseStatusCodePages()\n                //.UseMiddleware<TimingMiddleware>()\n                .UseMiddleware<ValidateIdsMiddleware>()\n                .UseEndpoints(fun endpointBuilder ->\n                    // Add Giraffe (Web API) endpoints\n                    endpointBuilder.MapGiraffeEndpoints(endpoints)\n\n                    // Add Prometheus scraping endpoint\n                    endpointBuilder.MapPrometheusScrapingEndpoint()\n                    |> ignore\n\n                    // Add SignalR hub endpoints\n                    endpointBuilder.MapHub<Notification.NotificationHub>(\"/notifications\")\n                    |> ignore)\n\n                // If we get here, we didn't find a route.\n                .UseGiraffe(\n                    notFoundHandler\n                )\n\n            // Set the global ApplicationContext.\n            ApplicationContext.serviceProvider <- app.ApplicationServices\n            ApplicationContext.Set().Wait()\n\n            logToConsole $\"Grace Server started successfully.\"\n"
  },
  {
    "path": "src/Grace.Server/Storage.Server.fs",
    "content": "namespace Grace.Server\n\nopen Azure.Core\nopen Azure.Storage.Blobs\nopen Azure.Storage.Blobs.Models\nopen Azure.Storage.Sas\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Shared.Parameters.Storage\nopen Grace.Shared.Utilities\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Repository\nopen Grace.Types.Types\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Linq\nopen System.Threading.Tasks\nopen System.IO\nopen System.Text\nopen Azure.Storage\nopen System.Diagnostics\nopen System.Reflection.Metadata\nopen System.Net.Http.Json\n\nmodule Storage =\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Storage.Server\")\n\n    /// Gets the metadata stored in the object storage provider for the specified file.\n    let getFileMetadata (repositoryDto: RepositoryDto) (fileVersion: FileVersion) (context: HttpContext) =\n        task {\n            match repositoryDto.ObjectStorageProvider with\n            | AzureBlobStorage ->\n                let! blobClient = getAzureBlobClientForFileVersion repositoryDto fileVersion (getCorrelationId context)\n                let! azureResponse = blobClient.GetPropertiesAsync()\n                let blobProperties = azureResponse.Value\n                return Ok(blobProperties.Metadata :?> IReadOnlyDictionary<string, string>)\n            | AWSS3 -> return Error(getErrorMessage StorageError.NotImplemented)\n            | GoogleCloudStorage -> return Error(getErrorMessage StorageError.NotImplemented)\n            | ObjectStorageProvider.Unknown ->\n                logToConsole\n                    $\"Error: Unknown ObjectStorageProvider in getFileMetadata for repository {repositoryDto.RepositoryId} - {repositoryDto.RepositoryName}.\"\n\n                logToConsole (sprintf \"%A\" repositoryDto)\n                return Error(getErrorMessage StorageError.UnknownObjectStorageProvider)\n        }\n\n    /// Gets a download URI for the specified file version that can be used by a Grace client.\n    let GetDownloadUri: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = (getCorrelationId context)\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context.BindJsonAsync<GetDownloadUriParameters>()\n                    let repositoryActor = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n                    let! repositoryDto = repositoryActor.Get correlationId\n\n                    let! downloadUri = getUriWithReadSharedAccessSignatureForFileVersion repositoryDto parameters.FileVersion correlationId\n                    context.SetStatusCode StatusCodes.Status200OK\n                    //log.LogTrace(\"fileVersion: {fileVersion.RelativePath}; downloadUri: {downloadUri}\", [| parameters.FileVersion.RelativePath, downloadUri |])\n                    return! context.WriteStringAsync $\"{downloadUri}\"\n                with\n                | ex ->\n                    context.SetStatusCode StatusCodes.Status500InternalServerError\n                    return! context.WriteTextAsync $\"Error in {context.Request.Path} at {DateTime.Now.ToLongTimeString()}.\"\n            }\n\n    /// Gets an upload URI for the specified file version that can be used by a Grace client.\n    let GetUploadUris: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n                let uris = Dictionary<string, Uri>()\n\n                try\n                    let! parameters = context.BindJsonAsync<GetUploadUriParameters>()\n                    let repositoryActor = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n                    let! repositoryDto = repositoryActor.Get correlationId\n\n                    for fileVersion in parameters.FileVersions do\n                        let! uploadUri = getUriWithWriteSharedAccessSignatureForFileVersion repositoryDto fileVersion correlationId\n                        uris.Add(fileVersion.RelativePath, uploadUri)\n\n                    if log.IsEnabled(LogLevel.Debug) then\n                        let sb = stringBuilderPool.Get()\n\n                        try\n                            for kvp in uris do\n                                sb.AppendLine($\"fileVersion: {kvp.Key}; uploadUri: {kvp.Value}\")\n                                |> ignore\n\n                            log.LogDebug(\"In GetUploadUri(): Created {count} uri's for these files: {uploadUris}\", uris.Count, sb.ToString())\n                        finally\n                            stringBuilderPool.Return(sb)\n\n                    context.SetStatusCode StatusCodes.Status200OK\n                    return! context.WriteJsonAsync uris\n                with\n                | ex ->\n                    context.SetStatusCode StatusCodes.Status500InternalServerError\n                    logToConsole $\"Exception in GetUploadUri: {(ExceptionResponse.Create ex)}\"\n\n                    return! context.WriteTextAsync $\"{getCurrentInstantExtended ()} Error in {context.Request.Path} at {DateTime.Now.ToLongTimeString()}.\"\n            }\n\n    /// Checks if a list of files already exists in object storage, and if any do not, return Uri's that the client can use to upload the file.\n    let GetUploadMetadataForFiles: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n                let correlationId = getCorrelationId context\n                let graceIds = getGraceIds context\n\n                try\n                    let! parameters = context.BindJsonAsync<GetUploadMetadataForFilesParameters>()\n\n                    Activity.Current.SetTag(\"fileVersions.Count\", $\"{parameters.FileVersions.Length}\")\n                    |> ignore\n\n                    if parameters.FileVersions.Length > 0 then\n                        let repositoryActor = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n                        let! repositoryDto = repositoryActor.Get correlationId\n\n                        let uploadMetadata = ConcurrentQueue<UploadMetadata>()\n\n                        do!\n                            Parallel.ForEachAsync(\n                                parameters.FileVersions,\n                                Constants.ParallelOptions,\n                                (fun fileVersion ct ->\n                                    ValueTask(\n                                        task {\n                                            //let! fileExists = fileExists repositoryDto fileVersion context\n\n                                            //if not <| fileExists then\n                                            let! blobUriWithSasToken =\n                                                getUriWithWriteSharedAccessSignatureForFileVersion repositoryDto fileVersion correlationId\n\n                                            uploadMetadata.Enqueue(\n                                                {\n                                                    RelativePath = fileVersion.RelativePath\n                                                    BlobUriWithSasToken = blobUriWithSasToken\n                                                    Sha256Hash = fileVersion.Sha256Hash\n                                                }\n                                            )\n                                        }\n                                    ))\n                            )\n\n                        Activity.Current.SetTag(\"uploadMetadata.Count\", $\"{uploadMetadata.Count}\")\n                        |> ignore\n\n                        context\n                            .GetLogger()\n                            .LogInformation(\n                                $\"{getCurrentInstantExtended ()} Received {parameters.FileVersions.Count} FileVersions; Returning {uploadMetadata.Count} uploadMetadata records.\"\n                            )\n\n                        return!\n                            context\n                            |> result200Ok (GraceReturnValue.Create (uploadMetadata.ToArray()) correlationId)\n                    else\n                        return!\n                            context\n                            |> result400BadRequest (GraceError.Create (getErrorMessage StorageError.FilesMustNotBeEmpty) correlationId)\n                with\n                | ex ->\n                    logToConsole $\"Exception in GetUploadMetadataForFiles: {(ExceptionResponse.Create ex)}\"\n\n                    return!\n                        context\n                        |> result500ServerError (GraceError.Create (getErrorMessage StorageError.ObjectStorageException) correlationId)\n            }\n\n    /// Deletes all documents from Cosmos DB. After calling, the web connection will time-out, but the method will continue to run until Cosmos DB is empty.\n    ///\n    /// **** This method is implemented only in Debug configuration. It is a no-op in Release configuration. ****\n    let DeleteAllFromCosmosDB: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n#if DEBUG\n                let correlationId = getCorrelationId context\n                let log = context.GetLogger()\n\n                log.LogWarning(\"{CurrentInstant} Deleting all rows from Cosmos DB.\", getCurrentInstantExtended ())\n\n                let! failed = deleteAllFromCosmosDb ()\n\n                if failed |> Seq.isEmpty then\n                    log.LogWarning(\"{CurrentInstant} Succeeded deleting all rows from CosmosDB.\", getCurrentInstantExtended ())\n\n                    return!\n                        context\n                        |> result200Ok (GraceReturnValue.Create \"Succeeded deleting all rows from Cosmos DB.\" (getCorrelationId context))\n                else\n                    let sb = stringBuilderPool.Get()\n\n                    try\n                        for fail in failed do\n                            sb.AppendLine(fail) |> ignore\n\n                        log.LogWarning(\n                            \"{CurrentInstant} Failed to delete all rows from Cosmos DB. Failures: {failedCount}.\",\n                            getCurrentInstantExtended (),\n                            failed.Count\n                        )\n\n                        log.LogWarning(sb.ToString())\n\n                        return!\n                            context\n                            |> result500ServerError (\n                                GraceError.Create\n                                    $\"Failed to delete all rows from Cosmos DB. Failures: {failed.Count}.{Environment.NewLine}{sb.ToString()}\"\n                                    (getCorrelationId context)\n                            )\n                    finally\n                        stringBuilderPool.Return(sb)\n#else\n                return! context |> result404NotFound\n#endif\n            }\n\n    /// Deletes all reminders from Cosmos DB. After calling, the web connection will time-out, but the method will continue to run until Cosmos DB is empty.\n    ///\n    /// **** This method is implemented only in Debug configuration. It is a no-op in Release configuration. ****\n    let DeleteAllRemindersFromCosmosDB: HttpHandler =\n        fun (next: HttpFunc) (context: HttpContext) ->\n            task {\n#if DEBUG\n                let correlationId = getCorrelationId context\n                let log = context.GetLogger()\n\n                log.LogWarning(\"{CurrentInstant} Deleting all reminders from Cosmos DB.\", getCurrentInstantExtended ())\n\n                let! failed = deleteAllRemindersFromCosmosDb ()\n\n                if failed |> Seq.isEmpty then\n                    log.LogWarning(\"{CurrentInstant} Succeeded deleting all reminders from CosmosDB.\", getCurrentInstantExtended ())\n\n                    return!\n                        context\n                        |> result200Ok (GraceReturnValue.Create \"Succeeded deleting all reminders from Cosmos DB.\" (getCorrelationId context))\n                else\n                    let sb = stringBuilderPool.Get()\n\n                    try\n                        for fail in failed do\n                            sb.AppendLine(fail) |> ignore\n\n                        log.LogWarning(\n                            \"{CurrentInstant} Failed to delete all reminders from Cosmos DB. Failures: {failedCount}.\",\n                            getCurrentInstantExtended (),\n                            failed.Count\n                        )\n\n                        log.LogWarning(sb.ToString())\n\n                        return!\n                            context\n                            |> result500ServerError (\n                                GraceError.Create\n                                    $\"Failed to delete all reminders from Cosmos DB. Failures: {failed.Count}.{Environment.NewLine}{sb.ToString()}\"\n                                    (getCorrelationId context)\n                            )\n                    finally\n                        stringBuilderPool.Return(sb)\n#else\n                return! context |> result404NotFound\n#endif\n            }\n"
  },
  {
    "path": "src/Grace.Server/ValidationResult.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Validation\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.AspNetCore.Http\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule ValidationResult =\n    let activitySource = new ActivitySource(\"ValidationResult\")\n\n    let internal hasPromotionSetScope (promotionSetId: string) = not (String.IsNullOrWhiteSpace promotionSetId)\n\n    let internal isValidStepsComputationAttempt (stepsComputationAttempt: int) = stepsComputationAttempt > 0\n\n    let private getPrincipal (context: HttpContext) =\n        if\n            isNull context.User\n            || isNull context.User.Identity\n            || String.IsNullOrWhiteSpace(context.User.Identity.Name)\n        then\n            Grace.Shared.Constants.GraceSystemUser\n        else\n            context.User.Identity.Name\n\n    let private parseOptionalGuid (rawValue: string) = if String.IsNullOrWhiteSpace(rawValue) then None else Some(Guid.Parse(rawValue))\n\n    let internal validationsForRecord (parameters: RecordValidationResultParameters) =\n        [|\n            (if String.IsNullOrWhiteSpace(parameters.ValidationResultId) then\n                 Ok() |> returnValueTask\n             else\n                 Guid.isValidAndNotEmptyGuid parameters.ValidationResultId ValidationResultError.InvalidValidationResultId)\n            (if String.IsNullOrWhiteSpace(parameters.ValidationSetId) then\n                 Ok() |> returnValueTask\n             else\n                 Guid.isValidAndNotEmptyGuid parameters.ValidationSetId ValidationResultError.InvalidValidationSetId)\n            (if String.IsNullOrWhiteSpace(parameters.PromotionSetId) then\n                 Ok() |> returnValueTask\n             else\n                 Guid.isValidAndNotEmptyGuid parameters.PromotionSetId ValidationResultError.InvalidPromotionSetId)\n            (if String.IsNullOrWhiteSpace(parameters.PromotionSetStepId) then\n                 Ok() |> returnValueTask\n             else\n                 Guid.isValidAndNotEmptyGuid parameters.PromotionSetStepId ValidationResultError.InvalidPromotionSetStepId)\n            String.isNotEmpty parameters.ValidationName ValidationResultError.ValidationNameRequired\n            String.isNotEmpty parameters.ValidationVersion ValidationResultError.ValidationVersionRequired\n            DiscriminatedUnion.isMemberOf<ValidationStatus, ValidationResultError> parameters.Status ValidationResultError.InvalidValidationStatus\n            (if parameters.ArtifactIds\n                |> Seq.forall (fun artifactId ->\n                    if String.IsNullOrWhiteSpace artifactId then\n                        false\n                    else\n                        let mutable parsed = Guid.Empty\n\n                        Guid.TryParse(artifactId, &parsed)\n                        && parsed <> Guid.Empty) then\n                 Ok()\n             else\n                 Error ValidationResultError.InvalidArtifactId)\n            |> returnValueTask\n            (if hasPromotionSetScope parameters.PromotionSetId then\n                 if parameters.StepsComputationAttempt < 0 then\n                     Error ValidationResultError.StepsComputationAttemptRequired\n                 elif isValidStepsComputationAttempt parameters.StepsComputationAttempt then\n                     Ok()\n                 else\n                     Error ValidationResultError.InvalidStepsComputationAttempt\n             else\n                 Ok())\n            |> returnValueTask\n        |]\n\n    /// Records a validation result.\n    let Record: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            async {\n                use activity = activitySource.StartActivity(\"Record\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<RecordValidationResultParameters>\n                    |> Async.AwaitTask\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validationResults = validationsForRecord parameters\n                let! validationsPassed = validationResults |> allPass |> Async.AwaitTask\n\n                if validationsPassed then\n                    let validationStatus =\n                        discriminatedUnionFromString<ValidationStatus> parameters.Status\n                        |> Option.get\n\n                    let artifactIds =\n                        parameters.ArtifactIds\n                        |> Seq.map Guid.Parse\n                        |> Seq.toList\n\n                    let validationResultId =\n                        if String.IsNullOrWhiteSpace(parameters.ValidationResultId) then\n                            Guid.NewGuid()\n                        else\n                            Guid.Parse(parameters.ValidationResultId)\n\n                    let validationSetId = parseOptionalGuid parameters.ValidationSetId\n\n                    let promotionSetId = parseOptionalGuid parameters.PromotionSetId\n\n                    let promotionSetStepId = parseOptionalGuid parameters.PromotionSetStepId\n\n                    let stepsComputationAttempt =\n                        if hasPromotionSetScope parameters.PromotionSetId then\n                            Some parameters.StepsComputationAttempt\n                        else\n                            None\n\n                    let validationResultDto: ValidationResultDto =\n                        { ValidationResultDto.Default with\n                            ValidationResultId = validationResultId\n                            OwnerId = graceIds.OwnerId\n                            OrganizationId = graceIds.OrganizationId\n                            RepositoryId = graceIds.RepositoryId\n                            ValidationSetId = validationSetId\n                            PromotionSetId = promotionSetId\n                            PromotionSetStepId = promotionSetStepId\n                            StepsComputationAttempt = stepsComputationAttempt\n                            ValidationName = parameters.ValidationName\n                            ValidationVersion = parameters.ValidationVersion\n                            Output = { Status = validationStatus; Summary = parameters.Summary; ArtifactIds = artifactIds }\n                            OnBehalfOf = [ UserId(getPrincipal context) ]\n                            CreatedAt = getCurrentInstant ()\n                        }\n\n                    let actorProxy = ValidationResult.CreateActorProxy validationResultId graceIds.RepositoryId correlationId\n                    let metadata = createMetadata context\n\n                    let! handleResult =\n                        actorProxy.Handle (ValidationResultCommand.Record validationResultDto) metadata\n                        |> Async.AwaitTask\n\n                    match handleResult with\n                    | Error graceError ->\n                        return!\n                            context\n                            |> result400BadRequest graceError\n                            |> Async.AwaitTask\n                    | Ok _ ->\n                        let! savedResult = actorProxy.Get correlationId |> Async.AwaitTask\n\n                        if savedResult.IsNone then\n                            let graceError =\n                                GraceError.Create (ValidationResultError.getErrorMessage ValidationResultError.FailedWhileApplyingEvent) correlationId\n\n                            return!\n                                context\n                                |> result500ServerError graceError\n                                |> Async.AwaitTask\n                        else\n                            let graceReturnValue =\n                                (GraceReturnValue.Create savedResult.Value correlationId)\n                                    .enhance(getParametersAsDictionary parameters)\n                                    .enhance(nameof OwnerId, graceIds.OwnerId)\n                                    .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                    .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                    .enhance(nameof ValidationResultId, validationResultId)\n                                    .enhance (\"Path\", context.Request.Path.Value)\n\n                            return!\n                                context\n                                |> result200Ok graceReturnValue\n                                |> Async.AwaitTask\n                else\n                    let! validationError =\n                        validationResults\n                        |> getFirstError\n                        |> Async.AwaitTask\n\n                    let errorMessage = ValidationResultError.getErrorMessage (validationError: ValidationResultError option)\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create errorMessage correlationId)\n                        |> Async.AwaitTask\n            }\n            |> Async.StartAsTask\n"
  },
  {
    "path": "src/Grace.Server/ValidationSet.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.Validation\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen Microsoft.AspNetCore.Http\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\n\nmodule ValidationSet =\n    type Validations<'T when 'T :> ValidationParameters> = 'T -> ValueTask<Result<unit, ValidationSetError>> array\n\n    let activitySource = new ActivitySource(\"ValidationSet\")\n\n    let private getPrincipal (context: HttpContext) =\n        if\n            isNull context.User\n            || isNull context.User.Identity\n            || String.IsNullOrWhiteSpace(context.User.Identity.Name)\n        then\n            Grace.Shared.Constants.GraceSystemUser\n        else\n            context.User.Identity.Name\n\n    let private parseValidationSetIdOrNew (rawValidationSetId: string) =\n        if String.IsNullOrWhiteSpace(rawValidationSetId) then\n            Ok(Guid.NewGuid())\n        else\n            let mutable parsed = Guid.Empty\n\n            if\n                Guid.TryParse(rawValidationSetId, &parsed)\n                && parsed <> Guid.Empty\n            then\n                Ok parsed\n            else\n                Error ValidationSetError.InvalidValidationSetId\n\n    let private processCommand<'T when 'T :> ValidationParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (validationSetId: ValidationSetId)\n        (command: ValidationSetCommand)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let metadata = createMetadata context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            let validationResults = validations parameters\n            let! validationsPassed = validationResults |> allPass\n\n            if validationsPassed then\n                let actorProxy = ValidationSet.CreateActorProxy validationSetId graceIds.RepositoryId correlationId\n\n                match! actorProxy.Handle command metadata with\n                | Ok graceReturnValue ->\n                    graceReturnValue\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof ValidationSetId, validationSetId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    return! context |> result200Ok graceReturnValue\n                | Error graceError ->\n                    graceError\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof ValidationSetId, validationSetId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    return! context |> result400BadRequest graceError\n            else\n                let! validationError = validationResults |> getFirstError\n                let graceError = GraceError.Create (ValidationSetError.getErrorMessage validationError) correlationId\n                return! context |> result400BadRequest graceError\n        }\n\n    let private processGet (context: HttpContext) (parameters: GetValidationSetParameters) (validationSetId: ValidationSetId) =\n        task {\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let actorProxy = ValidationSet.CreateActorProxy validationSetId graceIds.RepositoryId correlationId\n\n            match! actorProxy.Get correlationId with\n            | Some validationSet ->\n                let graceReturnValue =\n                    (GraceReturnValue.Create validationSet correlationId)\n                        .enhance(getParametersAsDictionary parameters)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(nameof ValidationSetId, validationSetId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result200Ok graceReturnValue\n            | None ->\n                let graceError = GraceError.Create (ValidationSetError.getErrorMessage ValidationSetError.ValidationSetDoesNotExist) correlationId\n                return! context |> result400BadRequest graceError\n        }\n\n    /// Creates a validation set.\n    let Create: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let! parameters = context |> parse<CreateValidationSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                match parseValidationSetIdOrNew parameters.ValidationSetId with\n                | Error validationSetError ->\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create (ValidationSetError.getErrorMessage validationSetError) (getCorrelationId context))\n                | Ok validationSetId ->\n                    let validations (_: CreateValidationSetParameters) =\n                        [|\n                            Guid.isValidAndNotEmptyGuid parameters.TargetBranchId ValidationSetError.InvalidTargetBranchId\n                            if parameters.Rules.IsEmpty then\n                                Error ValidationSetError.ValidationSetRulesRequired\n                            else\n                                Ok()\n                            |> returnValueTask\n                            if parameters.Validations.IsEmpty then\n                                Error ValidationSetError.ValidationDefinitionsRequired\n                            else\n                                Ok()\n                            |> returnValueTask\n                        |]\n\n                    let validationSetDto =\n                        { ValidationSetDto.Default with\n                            ValidationSetId = validationSetId\n                            OwnerId = graceIds.OwnerId\n                            OrganizationId = graceIds.OrganizationId\n                            RepositoryId = graceIds.RepositoryId\n                            TargetBranchId = Guid.Parse(parameters.TargetBranchId)\n                            Rules = parameters.Rules\n                            Validations = parameters.Validations\n                            CreatedBy = UserId(getPrincipal context)\n                            CreatedAt = getCurrentInstant ()\n                        }\n\n                    let command = ValidationSetCommand.Create validationSetDto\n                    return! processCommand context parameters validations validationSetId command\n            }\n\n    /// Gets a validation set.\n    let Get: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<GetValidationSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: GetValidationSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.ValidationSetId ValidationSetError.InvalidValidationSetId\n                    |]\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    let validationSetId = Guid.Parse(parameters.ValidationSetId)\n                    return! processGet context parameters validationSetId\n                else\n                    let! validationError = validationResults |> getFirstError\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create (ValidationSetError.getErrorMessage validationError) correlationId)\n            }\n\n    /// Updates a validation set.\n    let Update: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<UpdateValidationSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: UpdateValidationSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.ValidationSetId ValidationSetError.InvalidValidationSetId\n                        Guid.isValidAndNotEmptyGuid parameters.TargetBranchId ValidationSetError.InvalidTargetBranchId\n                        if parameters.Rules.IsEmpty then\n                            Error ValidationSetError.ValidationSetRulesRequired\n                        else\n                            Ok()\n                        |> returnValueTask\n                        if parameters.Validations.IsEmpty then\n                            Error ValidationSetError.ValidationDefinitionsRequired\n                        else\n                            Ok()\n                        |> returnValueTask\n                    |]\n\n                let validationSetId = Guid.Parse(parameters.ValidationSetId)\n                let actorProxy = ValidationSet.CreateActorProxy validationSetId graceIds.RepositoryId correlationId\n                let! currentValidationSet = actorProxy.Get correlationId\n\n                match currentValidationSet with\n                | None ->\n                    return!\n                        context\n                        |> result400BadRequest (\n                            GraceError.Create (ValidationSetError.getErrorMessage ValidationSetError.ValidationSetDoesNotExist) correlationId\n                        )\n                | Some currentValidationSet ->\n                    let updatedValidationSet =\n                        { currentValidationSet with\n                            TargetBranchId = Guid.Parse(parameters.TargetBranchId)\n                            Rules = parameters.Rules\n                            Validations = parameters.Validations\n                        }\n\n                    let command = ValidationSetCommand.Update updatedValidationSet\n                    return! processCommand context parameters validations validationSetId command\n            }\n\n    /// Logically deletes a validation set.\n    let Delete: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<DeleteValidationSetParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations (_: DeleteValidationSetParameters) =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.ValidationSetId ValidationSetError.InvalidValidationSetId\n                    |]\n\n                let validationSetId = Guid.Parse(parameters.ValidationSetId)\n\n                let command = ValidationSetCommand.DeleteLogical(parameters.Force, DeleteReason parameters.DeleteReason)\n\n                return! processCommand context parameters validations validationSetId command\n            }\n"
  },
  {
    "path": "src/Grace.Server/Validations.Server.fs",
    "content": "namespace Grace.Server\n\nopen FSharpPlus\nopen Giraffe\nopen Grace.Actors\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Extensions.MemoryCache\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Shared.Constants\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.Threading.Tasks\nopen Services\nopen Microsoft.Extensions.Caching.Memory\n\nmodule Validations =\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"Validations.Server\")\n\n    /// Converts an Option to a Result<unit, 'TError> to match the format of validation functions.\n    let optionToResult<'TError, 'T> (error: 'TError) (option: Task<'T option>) =\n        task {\n            match! option with\n            | Some value -> return Ok()\n            | None -> return Error error\n        }\n\n    module Owner =\n\n        /// Validates that the given ownerId exists in the database.\n        let ownerIdExists<'T> (ownerId: string) correlationId (error: 'T) =\n            task {\n                let mutable ownerGuid = Guid.Empty\n\n                if\n                    (not <| String.IsNullOrEmpty(ownerId))\n                    && Guid.TryParse(ownerId, &ownerGuid)\n                then\n                    match memoryCache.GetOwnerIdEntry ownerGuid with\n                    | Some value ->\n                        match value with\n                        | MemoryCache.Exists -> return Ok()\n                        | MemoryCache.DoesNotExist -> return Error error\n                        | _ ->\n                            return!\n                                ownerExists ownerId correlationId\n                                |> optionToResult error\n                    | None ->\n                        return!\n                            ownerExists ownerId correlationId\n                            |> optionToResult error\n                else\n                    return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that the given ownerId does not already exist in the database.\n        let ownerIdDoesNotExist<'T> (ownerId: string) correlationId (error: 'T) =\n            task {\n                let mutable ownerGuid = Guid.Empty\n                //logToConsole $\"In ownerIdDoesNotExist: ownerId: {ownerId}; correlationId: {correlationId}\"\n                if\n                    (not <| String.IsNullOrEmpty(ownerId))\n                    && Guid.TryParse(ownerId, &ownerGuid)\n                then\n                    let ownerActorProxy = Owner.CreateActorProxy ownerGuid correlationId\n                    //logToConsole $\"In ownerIdDoesNotExist: ownerActorProxy: {serialize ownerActorProxy}\"\n                    let! exists = ownerActorProxy.Exists correlationId\n                    if exists then return Error error else return Ok()\n                else\n                    return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that the owner exists in the database.\n        let ownerExists<'T> ownerId ownerName (context: HttpContext) (error: 'T) =\n            let result =\n                let graceIds = getGraceIds context\n                if graceIds.HasOwner then Ok() else Error error\n\n            ValueTask.FromResult(result)\n\n        /// Validates that the given ownerName does not already exist in the database.\n        let ownerNameDoesNotExist<'T> (ownerName: string) correlationId (error: 'T) =\n            task {\n                let! ownerNameExists = ownerNameExists ownerName false correlationId\n                if ownerNameExists then return Error error else return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the owner is deleted.\n        let ownerIsDeleted<'T> context correlationId (error: 'T) =\n            task {\n                let graceIds = getGraceIds context\n                let ownerGuid = Guid.Parse(graceIds.OwnerIdString)\n\n                match memoryCache.GetDeletedOwnerIdEntry ownerGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.DoesNotExist -> return Ok()\n                    | MemoryCache.Exists -> return Error error\n                    | _ ->\n                        return!\n                            ownerIsDeleted graceIds.OwnerIdString correlationId\n                            |> optionToResult error\n                | None ->\n                    return!\n                        ownerIsDeleted graceIds.OwnerIdString correlationId\n                        |> optionToResult error\n            }\n            |> ValidationResult\n\n        /// Validates that the owner is not deleted.\n        let ownerIsNotDeleted<'T> context correlationId (error: 'T) =\n            task {\n                match! ownerIsDeleted context correlationId error with\n                | Ok _ -> return Error error\n                | Error _ -> return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the given ownerId does not already exist in the database.\n        let ownerDoesNotExist<'T> ownerId ownerName correlationId (error: 'T) =\n            task {\n                if not <| String.IsNullOrEmpty(ownerId)\n                   && not <| String.IsNullOrEmpty(ownerName) then\n                    match! ownerExists ownerId ownerName correlationId error with\n                    | Ok _ -> return Error error\n                    | Error _ -> return Ok()\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n    module Organization =\n\n        /// Validates that the given organizationId exists in the database.\n        let organizationIdExists<'T> (organizationId: string) correlationId (error: 'T) =\n            task {\n                let mutable organizationGuid = Guid.Empty\n\n                if\n                    (not <| String.IsNullOrEmpty(organizationId))\n                    && Guid.TryParse(organizationId, &organizationGuid)\n                then\n                    match memoryCache.GetOrganizationIdEntry organizationGuid with\n                    | Some value ->\n                        match value with\n                        | MemoryCache.Exists -> return Ok()\n                        | MemoryCache.DoesNotExist -> return Error error\n                        | _ ->\n                            return!\n                                organizationExists organizationId correlationId\n                                |> optionToResult error\n                    | None ->\n                        return!\n                            organizationExists organizationId correlationId\n                            |> optionToResult error\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the given organizationId does not already exist in the database.\n        let organizationIdDoesNotExist<'T> (organizationId: string) correlationId (error: 'T) =\n            task {\n                if not <| String.IsNullOrEmpty(organizationId) then\n                    match! organizationIdExists organizationId correlationId error with\n                    | Ok _ -> return Error error\n                    | Error _ -> return Ok()\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the given organizationName does not already exist for this owner.\n        let organizationNameIsUniqueWithinOwner<'T>\n            (ownerId: string)\n            (ownerName: string)\n            (organizationName: string)\n            (context: HttpContext)\n            correlationId\n            (error: 'T)\n            =\n            task {\n                if not <| String.IsNullOrEmpty(organizationName) then\n                    let graceIds = getGraceIds context\n\n                    match! organizationNameIsUnique graceIds.OwnerIdString organizationName correlationId with\n                    | Ok isUnique ->\n                        //logToConsole\n                        //    $\"In organizationNameIsUnique: correlationId: {correlationId}; ownerId: {ownerId}; ownerName: {ownerName}; organizationName: {organizationName}; isUnique: {isUnique}\"\n\n                        if isUnique then return Ok() else return Error error\n                    | Error internalError ->\n                        logToConsole internalError\n                        return Error error\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the organization exists.\n        let organizationExists<'T> ownerId ownerName organizationId organizationName correlationId (error: 'T) =\n            task {\n                try\n                    let mutable organizationGuid = Guid.Empty\n\n                    if\n                        not <| String.IsNullOrEmpty(organizationId)\n                        && Guid.TryParse(organizationId, &organizationGuid)\n                    then\n                        match memoryCache.GetOrganizationIdEntry organizationGuid with\n                        | Some value ->\n                            match value with\n                            | MemoryCache.Exists -> return Ok()\n                            | MemoryCache.DoesNotExist -> return Error error\n                            | _ ->\n                                return!\n                                    organizationExists organizationId correlationId\n                                    |> optionToResult error\n                        | None ->\n                            return!\n                                organizationExists organizationId correlationId\n                                |> optionToResult error\n                    else\n                        return Error error\n                with\n                | ex ->\n                    log.LogError(ex, \"{CurrentInstant}: Exception in Grace.Server.Validations.organizationExists.\", getCurrentInstantExtended ())\n\n                    return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that the organization does not exist.\n        let organizationDoesNotExist<'T> ownerId ownerName organizationId organizationName correlationId (error: 'T) =\n            task {\n                match! organizationExists ownerId ownerName organizationId organizationName correlationId error with\n                | Ok _ -> return Error error\n                | Error error -> return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the organization is deleted.\n        let organizationIsDeleted<'T> context correlationId (error: 'T) =\n            task {\n                let graceIds = getGraceIds context\n                let organizationGuid = Guid.Parse(graceIds.OrganizationIdString)\n\n                match memoryCache.GetDeletedOrganizationIdEntry organizationGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.DoesNotExist -> return Ok()\n                    | MemoryCache.Exists -> return Error error\n                    | _ ->\n                        return!\n                            organizationIsDeleted graceIds.OrganizationIdString correlationId\n                            |> optionToResult error\n                | None ->\n                    return!\n                        organizationIsDeleted graceIds.OrganizationIdString correlationId\n                        |> optionToResult error\n            }\n            |> ValidationResult\n\n        /// Validates that the organization is not deleted.\n        let organizationIsNotDeleted<'T> context correlationId (error: 'T) =\n            task {\n                match! organizationIsDeleted context correlationId error with\n                | Ok _ -> return Error error\n                | Error _ -> return Ok()\n            }\n            |> ValidationResult\n\n    module Repository =\n\n        /// Validates that the given RepositoryId exists in the database.\n        let repositoryIdExists<'T> (organizationId: OrganizationId) (repositoryId: string) correlationId (error: 'T) =\n            task {\n                let mutable repositoryGuid = Guid.Empty\n\n                if\n                    (not <| String.IsNullOrEmpty(repositoryId))\n                    && Guid.TryParse(repositoryId, &repositoryGuid)\n                then\n                    match memoryCache.GetRepositoryIdEntry repositoryGuid with\n                    | Some value ->\n                        match value with\n                        | MemoryCache.Exists -> return Ok()\n                        | MemoryCache.DoesNotExist -> return Error error\n                        | _ ->\n                            return!\n                                repositoryExists organizationId repositoryId correlationId\n                                |> optionToResult error\n                    | None ->\n                        return!\n                            repositoryExists organizationId repositoryId correlationId\n                            |> optionToResult error\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the given repositoryId does not already exist in the database.\n        let repositoryIdDoesNotExist<'T> (organizationId: OrganizationId) (repositoryId: string) correlationId (error: 'T) =\n            task {\n                if not <| String.IsNullOrEmpty(repositoryId) then\n                    match! repositoryIdExists organizationId repositoryId correlationId error with\n                    | Ok _ -> return Error error\n                    | Error _ -> return Ok()\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the repository exists.\n        let repositoryExists<'T> ownerId ownerName organizationId organizationName repositoryId repositoryName correlationId (error: 'T) =\n            task {\n                match! resolveRepositoryId ownerId organizationId repositoryId repositoryName correlationId with\n                | Some repositoryId ->\n                    let exists = memoryCache.Get<string>(repositoryId)\n\n                    match exists with\n                    | MemoryCache.Exists -> return Ok()\n                    | MemoryCache.DoesNotExist -> return Error error\n                    | _ ->\n                        let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryId correlationId\n\n                        let! exists = repositoryActorProxy.Exists correlationId\n\n                        if exists then\n                            use newCacheEntry =\n                                memoryCache.CreateEntry(\n                                    repositoryId,\n                                    Value = MemoryCache.Exists,\n                                    AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                                )\n\n                            return Ok()\n                        else\n                            return Error error\n                | None -> return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that the repository is deleted.\n        let repositoryIsDeleted<'T> context correlationId (error: 'T) =\n            task {\n                let graceIds = getGraceIds context\n                let repositoryGuid = Guid.Parse(graceIds.RepositoryIdString)\n\n                match memoryCache.GetDeletedRepositoryIdEntry repositoryGuid with\n                | Some value ->\n                    match value with\n                    | MemoryCache.DoesNotExist -> return Ok()\n                    | MemoryCache.Exists -> return Error error\n                    | _ ->\n                        return!\n                            repositoryIsDeleted graceIds.OrganizationId graceIds.RepositoryIdString correlationId\n                            |> optionToResult error\n                | None ->\n                    return!\n                        repositoryIsDeleted graceIds.OrganizationId graceIds.RepositoryIdString correlationId\n                        |> optionToResult error\n            }\n            |> ValidationResult\n\n        /// Validates that the repository is not deleted.\n        let repositoryIsNotDeleted<'T> context correlationId (error: 'T) =\n            task {\n                match! repositoryIsDeleted context correlationId error with\n                | Ok _ -> return Error error\n                | Error _ -> return Ok()\n            }\n            |> ValidationResult\n\n        let repositoryNameIsUnique<'T> ownerId organizationId repositoryName correlationId (error: 'T) =\n            task {\n                if not <| String.IsNullOrEmpty(repositoryName) then\n                    match! repositoryNameIsUnique ownerId organizationId repositoryName correlationId with\n                    | Ok isUnique -> if isUnique then return Ok() else return Error error\n                    | Error internalError ->\n                        logToConsole internalError\n                        return Error error\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n    module Branch =\n\n        /// Validates that the given branchId exists in the database.\n        let branchIdExists<'T> (branchId: string) repositoryId correlationId (error: 'T) =\n            task {\n                let mutable branchGuid = Guid.Empty\n\n                if\n                    (not <| String.IsNullOrEmpty(branchId))\n                    && Guid.TryParse(branchId, &branchGuid)\n                then\n                    match memoryCache.GetBranchIdEntry branchGuid with\n                    | Some value ->\n                        match value with\n                        | MemoryCache.Exists -> return Ok()\n                        | MemoryCache.DoesNotExist -> return Error error\n                        | _ ->\n                            return!\n                                branchExists branchGuid repositoryId correlationId\n                                |> optionToResult error\n                    | None ->\n                        return!\n                            branchExists branchGuid repositoryId correlationId\n                            |> optionToResult error\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the given branchId does not exist in the database.\n        let branchIdDoesNotExist<'T> (branchId: string) repositoryId correlationId (error: 'T) =\n            task {\n                let mutable branchGuid = Guid.Empty\n\n                if\n                    (not <| String.IsNullOrEmpty(branchId))\n                    && Guid.TryParse(branchId, &branchGuid)\n                then\n                    let branchActorProxy = Branch.CreateActorProxy branchGuid repositoryId correlationId\n\n                    let! exists = branchActorProxy.Exists correlationId\n                    if exists then return Error error else return Ok()\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the branch exists in the database.\n        let branchExists<'T> ownerId organizationId repositoryId branchId branchName correlationId (error: 'T) =\n            task {\n                match! resolveBranchId ownerId organizationId repositoryId branchId branchName correlationId with\n                | Some branchId ->\n                    let exists = memoryCache.Get<string>(branchId)\n\n                    match exists with\n                    | MemoryCache.Exists -> return Ok()\n                    | MemoryCache.DoesNotExist -> return Error error\n                    | _ ->\n                        let branchActorProxy = Branch.CreateActorProxy branchId repositoryId correlationId\n\n                        let! exists = branchActorProxy.Exists correlationId\n\n                        if exists then\n                            use newCacheEntry =\n                                memoryCache.CreateEntry(\n                                    branchId,\n                                    Value = MemoryCache.Exists,\n                                    AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                                )\n\n                            return Ok()\n                        else\n                            return Error error\n                | None -> return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that a branch allows a specific reference type.\n        let branchAllowsReferenceType<'T> ownerId organizationId repositoryId branchId branchName (referenceType: ReferenceType) correlationId (error: 'T) =\n            task {\n                let mutable guid = Guid.Empty\n\n                match! resolveBranchId ownerId organizationId repositoryId branchId branchName correlationId with\n                | Some branchId ->\n                    let mutable allowed = new obj ()\n\n                    if memoryCache.TryGetValue($\"{branchId}{referenceType}Allowed\", &allowed) then\n                        let allowed = allowed :?> bool\n                        if allowed then return Ok() else return Error error\n                    else\n                        let branchActorProxy = Branch.CreateActorProxy branchId repositoryId correlationId\n\n                        let! branchDto = branchActorProxy.Get correlationId\n\n                        let allowed =\n                            match referenceType with\n                            | Promotion -> if branchDto.PromotionEnabled then true else false\n                            | Commit -> if branchDto.CommitEnabled then true else false\n                            | Checkpoint -> if branchDto.CheckpointEnabled then true else false\n                            | Save -> if branchDto.SaveEnabled then true else false\n                            | Tag -> if branchDto.TagEnabled then true else false\n                            | External -> if branchDto.ExternalEnabled then true else false\n                            | Rebase -> true // Rebase is always allowed.\n\n                        use newCacheEntry =\n                            memoryCache.CreateEntry(\n                                $\"{branchId}{referenceType}Allowed\",\n                                Value = allowed,\n                                AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                            )\n\n                        if allowed then return Ok() else return Error error\n                | None -> return Error error\n            }\n            |> ValidationResult\n\n\n        /// Validates that a branch allows assign to create promotion references.\n        let branchAllowsAssign<'T> ownerId organizationId repositoryId branchId branchName correlationId (error: 'T) =\n            task {\n                let mutable guid = Guid.Empty\n\n                match! resolveBranchId ownerId organizationId repositoryId branchId branchName correlationId with\n                | Some branchId ->\n                    let mutable allowed = new obj ()\n\n                    if memoryCache.TryGetValue($\"{branchId}AssignAllowed\", &allowed) then\n                        let allowed = allowed :?> bool\n                        if allowed then return Ok() else return Error error\n                    else\n                        let branchActorProxy = Branch.CreateActorProxy branchId repositoryId correlationId\n\n                        let! branchDto = branchActorProxy.Get correlationId\n                        let allowed = branchDto.AssignEnabled\n\n                        use newCacheEntry =\n                            memoryCache.CreateEntry(\n                                $\"{branchId}AssignAllowed\",\n                                Value = allowed,\n                                AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                            )\n\n                        if allowed then return Ok() else return Error error\n                | None -> return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that a parent branch allows promotions.\n        let parentBranchAllowsPromotions<'T> ownerId organizationId repositoryId parentBranchId parentBranchName correlationId (error: 'T) =\n            task {\n                match! resolveBranchId ownerId organizationId repositoryId parentBranchId parentBranchName correlationId with\n                | Some resolvedParentBranchId ->\n                    let mutable allowed = new obj ()\n\n                    if memoryCache.TryGetValue($\"{resolvedParentBranchId}PromotionAllowed\", &allowed) then\n                        let allowed = allowed :?> bool\n                        if allowed then return Ok() else return Error error\n                    else\n                        let branchActorProxy = Branch.CreateActorProxy resolvedParentBranchId repositoryId correlationId\n\n                        let! branchDto = branchActorProxy.Get correlationId\n                        let allowed = branchDto.PromotionEnabled\n\n                        use newCacheEntry =\n                            memoryCache.CreateEntry(\n                                $\"{resolvedParentBranchId}PromotionAllowed\",\n                                Value = allowed,\n                                AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                            )\n\n                        if allowed then return Ok() else return Error error\n                | None -> return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that the given branchName does not exist in the database.\n        let branchNameDoesNotExist<'T> ownerId organizationId repositoryId branchName correlationId (error: 'T) =\n            task {\n                match! resolveBranchId ownerId organizationId repositoryId String.Empty branchName correlationId with\n                | Some branchId -> return Error error\n                | None -> return Ok()\n            }\n            |> ValidationResult\n\n        /// Validates that the given ReferenceId exists in the database.\n        let referenceIdExists<'T> (referenceId: ReferenceId) repositoryId correlationId (error: 'T) =\n            task {\n                if not <| (referenceId = Guid.Empty) then\n                    let referenceActorProxy = Reference.CreateActorProxy referenceId repositoryId correlationId\n\n                    let! exists = referenceActorProxy.Exists correlationId\n                    if exists then return Ok() else return Error error\n                else\n                    return Ok()\n            }\n            |> ValidationResult\n\n    module DirectoryVersion =\n        /// Validates that the given DirectoryId exists in the database.\n        let directoryIdExists<'T> (directoryId: DirectoryVersionId) repositoryId correlationId (error: 'T) =\n            task {\n                let exists = memoryCache.Get<string>(directoryId)\n\n                match exists with\n                | MemoryCache.Exists -> return Ok()\n                | MemoryCache.DoesNotExist -> return Error error\n                | _ ->\n                    let directoryVersionActorProxy = DirectoryVersion.CreateActorProxy directoryId repositoryId correlationId\n\n                    let! exists = directoryVersionActorProxy.Exists correlationId\n\n                    if exists then\n                        use newCacheEntry =\n                            memoryCache.CreateEntry(\n                                directoryId,\n                                Value = MemoryCache.Exists,\n                                AbsoluteExpirationRelativeToNow = MemoryCache.DefaultExpirationTime\n                            )\n\n                        return Ok()\n                    else\n                        return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that all of the given DirectoryIds exist in the database.\n        let directoryIdsExist<'T> (directoryIds: List<DirectoryVersionId>) repositoryId correlationId (error: 'T) =\n            task {\n                let mutable allExist = true\n                let directoryIdStack = Queue<DirectoryVersionId>(directoryIds)\n\n                while directoryIdStack.Count > 0 && allExist do\n                    let directoryId = directoryIdStack.Dequeue()\n                    let directoryVersionActorProxy = DirectoryVersion.CreateActorProxy directoryId repositoryId correlationId\n\n                    let! exists = directoryVersionActorProxy.Exists correlationId\n                    allExist <- exists\n\n                if allExist then return Ok() else return Error error\n            }\n            |> ValidationResult\n\n        /// Validates that a directory version with the provided Sha256Hash exists in a repository.\n        let sha256HashExists<'T> repositoryId sha256Hash correlationId (error: 'T) =\n            task {\n                let repositoryActorProxy = Repository.CreateActorProxy repositoryId correlationId\n\n                match! getDirectoryVersionBySha256Hash repositoryId sha256Hash correlationId with\n                | Some directoryVersion -> return Ok()\n                | None -> return Error error\n            }\n            |> ValidationResult\n"
  },
  {
    "path": "src/Grace.Server/WorkItem.Server.fs",
    "content": "namespace Grace.Server\n\nopen Giraffe\nopen Grace.Actors.Constants\nopen Grace.Actors.Extensions.ActorProxy\nopen Grace.Actors.Interfaces\nopen Grace.Actors.Services\nopen Grace.Server.ApplicationContext\nopen Grace.Server.Services\nopen Grace.Shared\nopen Grace.Shared.Extensions\nopen Grace.Shared.Parameters.WorkItem\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Artifact\nopen Grace.Types.WorkItem\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.AspNetCore.Http\nopen Microsoft.Extensions.Logging\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.Security.Cryptography\nopen System.Text\nopen System.Threading.Tasks\n\nmodule WorkItem =\n    type Validations<'T when 'T :> WorkItemParameters> = 'T -> ValueTask<Result<unit, WorkItemError>> array\n\n    type WorkItemIdentifier =\n        | Id of WorkItemId\n        | Number of WorkItemNumber\n\n    let log = ApplicationContext.loggerFactory.CreateLogger(\"WorkItem.Server\")\n\n    let activitySource = new ActivitySource(\"WorkItem\")\n\n    let private tryParseWorkItemIdentifier (value: string) =\n        let mutable parsedGuid = Guid.Empty\n\n        if not <| String.IsNullOrWhiteSpace(value)\n           && Guid.TryParse(value, &parsedGuid)\n           && parsedGuid <> Guid.Empty then\n            Ok(Id parsedGuid)\n        else\n            let mutable parsedNumber = 0L\n\n            if\n                not <| String.IsNullOrWhiteSpace(value)\n                && Int64.TryParse(value, &parsedNumber)\n            then\n                if parsedNumber > 0L then\n                    Ok(Number parsedNumber)\n                else\n                    Error WorkItemError.InvalidWorkItemNumber\n            else\n                Error WorkItemError.InvalidWorkItemId\n\n    let internal validateWorkItemIdentifier (value: string) =\n        match tryParseWorkItemIdentifier value with\n        | Ok _ -> Ok() |> returnValueTask\n        | Error error -> Error error |> returnValueTask\n\n    let private resolveWorkItemId (repositoryId: RepositoryId) (workItemIdentifier: string) (correlationId: CorrelationId) =\n        task {\n            match tryParseWorkItemIdentifier workItemIdentifier with\n            | Error error -> return Error(GraceError.Create (WorkItemError.getErrorMessage error) correlationId)\n            | Ok identifier ->\n                match identifier with\n                | Id workItemId -> return Ok workItemId\n                | Number workItemNumber ->\n                    let workItemNumberActorProxy = WorkItemNumber.CreateActorProxy repositoryId correlationId\n                    let! cachedWorkItemId = workItemNumberActorProxy.GetWorkItemId workItemNumber correlationId\n\n                    match cachedWorkItemId with\n                    | Some workItemId -> return Ok workItemId\n                    | None ->\n                        let! persistedWorkItemId = getWorkItemIdByNumber repositoryId workItemNumber correlationId\n\n                        match persistedWorkItemId with\n                        | Some workItemId ->\n                            do! workItemNumberActorProxy.SetWorkItemId workItemNumber workItemId correlationId\n                            return Ok workItemId\n                        | None -> return Error(GraceError.Create (WorkItemError.getErrorMessage WorkItemError.WorkItemDoesNotExist) correlationId)\n        }\n\n    let private cacheWorkItemNumber (repositoryId: RepositoryId) (workItemNumber: WorkItemNumber) (workItemId: WorkItemId) (correlationId: CorrelationId) =\n        task {\n            let workItemNumberActorProxy = WorkItemNumber.CreateActorProxy repositoryId correlationId\n            do! workItemNumberActorProxy.SetWorkItemId workItemNumber workItemId correlationId\n        }\n\n    let private withWorkItemNumberLock (repositoryId: RepositoryId) (correlationId: CorrelationId) (work: unit -> Task<GraceResult<string>>) =\n        task {\n            let lockName = $\"workitem-number|{repositoryId}\"\n            let lockOwner = $\"WorkItemCreate:{correlationId}\"\n            let lockActorProxy = GlobalLock.CreateActorProxy lockName correlationId\n            let mutable acquired = false\n            let mutable attempt = 0\n\n            while not acquired && attempt < 100 do\n                let! acquiredNow = lockActorProxy.AcquireLock lockOwner\n\n                if acquiredNow then\n                    acquired <- true\n                else\n                    attempt <- attempt + 1\n                    do! Task.Delay(25)\n\n            if not acquired then\n                return Error(GraceError.Create \"Could not acquire lock while allocating WorkItemNumber.\" correlationId)\n            else\n                let! result =\n                    task {\n                        try\n                            return! work ()\n                        with\n                        | ex -> return Error(GraceError.CreateWithException ex String.Empty correlationId)\n                    }\n\n                let! releaseResult = lockActorProxy.ReleaseLock lockOwner\n\n                match releaseResult with\n                | Ok _ -> ()\n                | Error releaseError ->\n                    log.LogWarning(\n                        \"{CurrentInstant}: Failed to release lock for WorkItemNumber allocation. CorrelationId: {correlationId}; RepositoryId: {repositoryId}; Error: {releaseError}.\",\n                        getCurrentInstantExtended (),\n                        correlationId,\n                        repositoryId,\n                        releaseError\n                    )\n\n                return result\n        }\n\n    let processCommand<'T when 'T :> WorkItemParameters> (context: HttpContext) (validations: Validations<'T>) (command: 'T -> ValueTask<WorkItemCommand>) =\n        task {\n            let commandName = context.Items[\"Command\"] :?> string\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = Dictionary<string, obj>()\n\n            try\n                use activity = activitySource.StartActivity(\"processCommand\", ActivityKind.Server)\n                let! parameters = context |> parse<'T>\n                parameterDictionary.AddRange(getParametersAsDictionary parameters)\n\n                // Use IDs from middleware.\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match! resolveWorkItemId graceIds.RepositoryId parameters.WorkItemId correlationId with\n                    | Error graceError ->\n                        graceError\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance (\"Path\", context.Request.Path.Value)\n                        |> ignore\n\n                        return! context |> result400BadRequest graceError\n                    | Ok workItemId ->\n                        let! cmd = command parameters\n                        let actorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n                        let metadata = createMetadata context\n\n                        match! actorProxy.Handle cmd metadata with\n                        | Ok graceReturnValue ->\n                            graceReturnValue\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof WorkItemId, workItemId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result200Ok graceReturnValue\n                        | Error graceError ->\n                            graceError\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof WorkItemId, workItemId)\n                                .enhance(\"Command\", commandName)\n                                .enhance (\"Path\", context.Request.Path.Value)\n                            |> ignore\n\n                            return! context |> result400BadRequest graceError\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(\"Command\", commandName)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                log.LogError(\n                    ex,\n                    \"{CurrentInstant}: Exception in WorkItem.Server.processCommand. CorrelationId: {correlationId}.\",\n                    getCurrentInstantExtended (),\n                    correlationId\n                )\n\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let processQuery<'T, 'U when 'T :> WorkItemParameters>\n        (context: HttpContext)\n        (parameters: 'T)\n        (validations: Validations<'T>)\n        (query: QueryResult<IWorkItemActor, 'U>)\n        =\n        task {\n            use activity = activitySource.StartActivity(\"processQuery\", ActivityKind.Server)\n            let graceIds = getGraceIds context\n            let correlationId = getCorrelationId context\n            let parameterDictionary = getParametersAsDictionary parameters\n\n            try\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match! resolveWorkItemId graceIds.RepositoryId parameters.WorkItemId correlationId with\n                    | Error graceError ->\n                        graceError\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n                        |> ignore\n\n                        return! context |> result400BadRequest graceError\n                    | Ok workItemId ->\n                        let actorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n                        let! queryResult = query context 0 actorProxy\n\n                        let graceReturnValue =\n                            (GraceReturnValue.Create queryResult correlationId)\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof WorkItemId, workItemId)\n                                .enhance (\"Path\", context.Request.Path.Value)\n\n                        return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            with\n            | ex ->\n                let graceError =\n                    (GraceError.CreateWithException ex String.Empty correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result500ServerError graceError\n        }\n\n    let internal buildUpdateCommands (parameters: UpdateWorkItemParameters) =\n        [\n            if not <| String.IsNullOrEmpty(parameters.Title) then\n                WorkItemCommand.SetTitle parameters.Title\n            if\n                not\n                <| String.IsNullOrEmpty(parameters.Description)\n            then\n                WorkItemCommand.SetDescription parameters.Description\n            if not <| String.IsNullOrEmpty(parameters.Status) then\n                let status =\n                    discriminatedUnionFromString<WorkItemStatus> parameters.Status\n                    |> Option.get\n\n                WorkItemCommand.SetStatus status\n            if\n                not\n                <| String.IsNullOrEmpty(parameters.Constraints)\n            then\n                WorkItemCommand.SetConstraints parameters.Constraints\n            if not <| String.IsNullOrEmpty(parameters.Notes) then\n                WorkItemCommand.SetNotes parameters.Notes\n            if\n                not\n                <| String.IsNullOrEmpty(parameters.ArchitecturalNotes)\n            then\n                WorkItemCommand.SetArchitecturalNotes parameters.ArchitecturalNotes\n            if\n                not\n                <| String.IsNullOrEmpty(parameters.MigrationNotes)\n            then\n                WorkItemCommand.SetMigrationNotes parameters.MigrationNotes\n        ]\n\n    let internal validateLinkReferenceParameters (parameters: LinkReferenceParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n            Guid.isValidAndNotEmptyGuid parameters.ReferenceId WorkItemError.InvalidReferenceId\n        |]\n\n    let internal validateLinkArtifactParameters (parameters: LinkArtifactParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n            Guid.isValidAndNotEmptyGuid parameters.ArtifactId WorkItemError.InvalidArtifactId\n        |]\n\n    let internal validateLinkPromotionSetParameters (parameters: LinkPromotionSetParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n            Guid.isValidAndNotEmptyGuid parameters.PromotionSetId WorkItemError.InvalidPromotionSetId\n        |]\n\n    let internal parseRemovableArtifactType (artifactType: string) =\n        if String.IsNullOrWhiteSpace(artifactType) then\n            Error WorkItemError.InvalidArtifactType\n        elif\n            String.Equals(artifactType, \"summary\", StringComparison.OrdinalIgnoreCase)\n            || String.Equals(artifactType, \"agentsummary\", StringComparison.OrdinalIgnoreCase)\n        then\n            Ok ArtifactType.AgentSummary\n        elif String.Equals(artifactType, \"prompt\", StringComparison.OrdinalIgnoreCase) then\n            Ok ArtifactType.Prompt\n        elif\n            String.Equals(artifactType, \"notes\", StringComparison.OrdinalIgnoreCase)\n            || String.Equals(artifactType, \"reviewnotes\", StringComparison.OrdinalIgnoreCase)\n        then\n            Ok ArtifactType.ReviewNotes\n        else\n            Error WorkItemError.InvalidArtifactType\n\n    let internal parseAttachmentType (artifactType: string) = parseRemovableArtifactType artifactType\n\n    let internal validateListWorkItemAttachmentsParameters (parameters: ListWorkItemAttachmentsParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n        |]\n\n    let internal validateShowWorkItemAttachmentParameters (parameters: ShowWorkItemAttachmentParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n            String.isNotEmpty parameters.AttachmentType WorkItemError.InvalidArtifactType\n        |]\n\n    let internal validateDownloadWorkItemAttachmentParameters (parameters: DownloadWorkItemAttachmentParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n            Guid.isValidAndNotEmptyGuid parameters.ArtifactId WorkItemError.InvalidArtifactId\n        |]\n\n    let internal validateRemoveArtifactTypeLinksParameters (parameters: RemoveArtifactTypeLinksParameters) =\n        [|\n            validateWorkItemIdentifier parameters.WorkItemId\n            String.isNotEmpty parameters.ArtifactType WorkItemError.InvalidArtifactType\n        |]\n\n    let internal canonicalAddSummaryContractMessage =\n        \"Canonical add-summary requests must provide SummaryContent (required), PromptContent (optional), PromptOrigin (optional with PromptContent), and PromotionSetId (optional). Caller-supplied SummaryArtifactId/PromptArtifactId values are not supported.\"\n\n    let private tryParseNonEmptyGuid (value: string) =\n        let mutable parsed = Guid.Empty\n\n        if not <| String.IsNullOrWhiteSpace(value)\n           && Guid.TryParse(value, &parsed)\n           && parsed <> Guid.Empty then\n            Some parsed\n        else\n            None\n\n    let private resolveScopeId (resolvedId: Guid) (rawValue: string) =\n        if resolvedId <> Guid.Empty then\n            resolvedId\n        else\n            tryParseNonEmptyGuid rawValue\n            |> Option.defaultValue Guid.Empty\n\n    let private resolveWorkItemScopeIds (graceIds: GraceIds) (parameters: WorkItemParameters) =\n        let ownerId = resolveScopeId graceIds.OwnerId parameters.OwnerId\n        let organizationId = resolveScopeId graceIds.OrganizationId parameters.OrganizationId\n        let repositoryId = resolveScopeId graceIds.RepositoryId parameters.RepositoryId\n        ownerId, organizationId, repositoryId\n\n    let internal validateAddSummaryParameters (parameters: AddSummaryParameters) =\n        match tryParseWorkItemIdentifier parameters.WorkItemId with\n        | Error workItemError -> Error(WorkItemError.getErrorMessage workItemError)\n        | Ok _ ->\n            if String.IsNullOrWhiteSpace(parameters.SummaryContent) then\n                Error($\"SummaryContent is required. {canonicalAddSummaryContractMessage}\")\n            elif not\n                 <| String.IsNullOrWhiteSpace(parameters.SummaryArtifactId)\n                 || not\n                    <| String.IsNullOrWhiteSpace(parameters.PromptArtifactId) then\n                Error($\"Caller-supplied artifact IDs are not supported by add-summary. {canonicalAddSummaryContractMessage}\")\n            elif\n                not\n                <| String.IsNullOrWhiteSpace(parameters.PromptOrigin)\n                && String.IsNullOrWhiteSpace(parameters.PromptContent)\n            then\n                Error($\"PromptOrigin can only be provided when PromptContent is provided. {canonicalAddSummaryContractMessage}\")\n            elif not\n                 <| String.IsNullOrWhiteSpace(parameters.PromotionSetId)\n                 && (tryParseNonEmptyGuid parameters.PromotionSetId\n                     |> Option.isNone) then\n                Error \"PromotionSetId must be a valid non-empty Guid.\"\n            else\n                Ok()\n\n    let private normalizeAddSummaryMimeType (mimeType: string) = if String.IsNullOrWhiteSpace(mimeType) then \"text/markdown\" else mimeType.Trim()\n\n    let internal buildAddSummaryArtifactSeed (repositoryId: RepositoryId) (workItemId: WorkItemId) (artifactCorrelationId: CorrelationId) =\n        let normalizedCorrelationId =\n            if String.IsNullOrWhiteSpace(artifactCorrelationId) then\n                String.Empty\n            else\n                artifactCorrelationId.Trim().ToLowerInvariant()\n\n        let repositorySegment = repositoryId.ToString(\"N\")\n        let workItemSegment = workItemId.ToString(\"N\")\n\n        $\"{repositorySegment}|{workItemSegment}|{normalizedCorrelationId}\"\n\n    let private createDeterministicArtifactId (seed: string) =\n        let normalizedSeed =\n            if String.IsNullOrWhiteSpace(seed) then\n                String.Empty\n            else\n                seed.Trim().ToLowerInvariant()\n\n        let seedBytes = Encoding.UTF8.GetBytes(normalizedSeed)\n\n        use hasher = SHA256.Create()\n        let hash = hasher.ComputeHash(seedBytes)\n        let guidBytes = hash[0..15]\n        guidBytes[6] <- (guidBytes[6] &&& 0x0Fuy) ||| 0x50uy\n        guidBytes[8] <- (guidBytes[8] &&& 0x3Fuy) ||| 0x80uy\n        Guid(guidBytes)\n\n    let internal buildDeterministicAddSummaryArtifactId (repositoryId: RepositoryId) (workItemId: WorkItemId) (artifactCorrelationId: CorrelationId) =\n        buildAddSummaryArtifactSeed repositoryId workItemId artifactCorrelationId\n        |> createDeterministicArtifactId\n\n    let internal buildDeterministicAddSummaryBlobPath (artifactId: ArtifactId) = $\"grace-artifacts/by-id/{artifactId}\"\n\n    let private computeSha256 (contentBytes: byte array) =\n        use hasher = SHA256.Create()\n        let hash = hasher.ComputeHash(contentBytes)\n        Convert.ToHexString(hash).ToLowerInvariant()\n\n    let private isGraceTestingEnabled () =\n        match Environment.GetEnvironmentVariable(\"GRACE_TESTING\") with\n        | null -> false\n        | value ->\n            value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n            || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n            || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase)\n\n    let private uploadArtifactContent repositoryDto (blobPath: string) (contentBytes: byte array) (correlationId: CorrelationId) =\n        task {\n            try\n                use stream = new MemoryStream(contentBytes)\n                let! containerClient = getContainerClient repositoryDto correlationId\n                let! _ = containerClient.CreateIfNotExistsAsync()\n                let! containerExists = containerClient.ExistsAsync()\n\n                if not containerExists.Value then\n                    return Error(GraceError.Create $\"Artifact container '{containerClient.Name}' does not exist for blob path '{blobPath}'.\" correlationId)\n                else\n                    let blobClient = containerClient.GetBlobClient(blobPath)\n                    let! _ = blobClient.UploadAsync(stream, overwrite = true)\n                    return Ok()\n            with\n            | :? Azure.RequestFailedException as requestEx when\n                isGraceTestingEnabled ()\n                && String.Equals(requestEx.ErrorCode, \"ContainerNotFound\", StringComparison.OrdinalIgnoreCase)\n                ->\n                return Ok()\n            | ex -> return Error(GraceError.Create $\"Failed to upload artifact content: {ex.Message}\" correlationId)\n        }\n\n    let private withCorrelationId (metadata: EventMetadata) (correlationId: CorrelationId) = { metadata with CorrelationId = correlationId }\n\n    let private addSummaryCorrelationId (baseCorrelationId: CorrelationId) (segment: string) = $\"{baseCorrelationId}:add-summary:{segment}\"\n\n    let private isDuplicateCorrelationIdError (graceError: GraceError) =\n        String.Equals(graceError.Error, WorkItemError.getErrorMessage WorkItemError.DuplicateCorrelationId, StringComparison.OrdinalIgnoreCase)\n\n    let private isArtifactDuplicateCorrelationIdError (graceError: GraceError) =\n        String.Equals(graceError.Error, \"Duplicate correlation ID for Artifact command.\", StringComparison.OrdinalIgnoreCase)\n\n    let private isArtifactAlreadyExistsError (graceError: GraceError) =\n        String.Equals(graceError.Error, \"Artifact already exists.\", StringComparison.OrdinalIgnoreCase)\n\n    let private isRecoverableArtifactCreateError (graceError: GraceError) =\n        isArtifactDuplicateCorrelationIdError graceError\n        || isArtifactAlreadyExistsError graceError\n\n    let private handleWorkItemCommandAllowReplay (workItemActorProxy: IWorkItemActor) (command: WorkItemCommand) (metadata: EventMetadata) =\n        task {\n            match! workItemActorProxy.Handle command metadata with\n            | Ok _ -> return Ok()\n            | Error graceError when isDuplicateCorrelationIdError graceError -> return Ok()\n            | Error graceError -> return Error graceError\n        }\n\n    let private createArtifactFromContent\n        repositoryDto\n        (graceIds: GraceIds)\n        (workItemId: WorkItemId)\n        (metadata: EventMetadata)\n        (artifactType: ArtifactType)\n        (mimeType: string)\n        (content: string)\n        =\n        task {\n            let artifactId = buildDeterministicAddSummaryArtifactId graceIds.RepositoryId workItemId metadata.CorrelationId\n\n            let contentBytes = Encoding.UTF8.GetBytes(content)\n            let createdAt = metadata.Timestamp\n            let blobPath = buildDeterministicAddSummaryBlobPath artifactId\n\n            let artifactMetadata: ArtifactMetadata =\n                { ArtifactMetadata.Default with\n                    ArtifactId = artifactId\n                    OwnerId = graceIds.OwnerId\n                    OrganizationId = graceIds.OrganizationId\n                    RepositoryId = graceIds.RepositoryId\n                    ArtifactType = artifactType\n                    MimeType = normalizeAddSummaryMimeType mimeType\n                    Size = int64 contentBytes.LongLength\n                    Sha256 = Some(Sha256Hash(computeSha256 contentBytes))\n                    BlobPath = blobPath\n                    CreatedAt = createdAt\n                    CreatedBy = UserId metadata.Principal\n                }\n\n            let artifactActorProxy = Artifact.CreateActorProxy artifactId graceIds.RepositoryId metadata.CorrelationId\n\n            let! persistedArtifactMetadataResult =\n                task {\n                    match! artifactActorProxy.Handle (ArtifactCommand.Create artifactMetadata) metadata with\n                    | Ok _ -> return Ok artifactMetadata\n                    | Error graceError when isRecoverableArtifactCreateError graceError ->\n                        match! artifactActorProxy.Get metadata.CorrelationId with\n                        | Some existingMetadata -> return Ok existingMetadata\n                        | None -> return Error graceError\n                    | Error graceError -> return Error graceError\n                }\n\n            match persistedArtifactMetadataResult with\n            | Error graceError -> return Error graceError\n            | Ok persistedArtifactMetadata ->\n                if persistedArtifactMetadata.ArtifactType\n                   <> artifactType then\n                    return\n                        Error(\n                            GraceError.Create\n                                $\"Artifact '{artifactId}' already exists with type '{getDiscriminatedUnionCaseName persistedArtifactMetadata.ArtifactType}', expected '{getDiscriminatedUnionCaseName artifactType}'.\"\n                                metadata.CorrelationId\n                        )\n                else\n                    match! uploadArtifactContent repositoryDto persistedArtifactMetadata.BlobPath contentBytes metadata.CorrelationId with\n                    | Error graceError -> return Error graceError\n                    | Ok _ -> return Ok artifactId\n        }\n\n    /// Adds summary content (and optional prompt content) to a work item using the canonical add-summary request mode.\n    let AddSummary: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            async {\n                use activity = activitySource.StartActivity(\"AddSummary\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<AddSummaryParameters>\n                    |> Async.AwaitTask\n\n                let parameterDictionary = getParametersAsDictionary parameters\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let withContext (graceError: GraceError) =\n                    graceError\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, graceIds.OwnerId)\n                        .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                        .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                        .enhance(\"Command\", \"AddSummary\")\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    graceError\n\n                match validateAddSummaryParameters parameters with\n                | Error validationError ->\n                    return!\n                        context\n                        |> result400BadRequest (\n                            GraceError.Create validationError correlationId\n                            |> withContext\n                        )\n                        |> Async.AwaitTask\n                | Ok _ ->\n                    let! workItemIdResult =\n                        resolveWorkItemId graceIds.RepositoryId parameters.WorkItemId correlationId\n                        |> Async.AwaitTask\n\n                    match workItemIdResult with\n                    | Error graceError ->\n                        return!\n                            context\n                            |> result400BadRequest (graceError |> withContext)\n                            |> Async.AwaitTask\n                    | Ok workItemId ->\n                        let requestMetadata = createMetadata context\n                        let workItemActorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n                        let repositoryActorProxy = Repository.CreateActorProxy graceIds.OrganizationId graceIds.RepositoryId correlationId\n\n                        let! repositoryDto =\n                            repositoryActorProxy.Get correlationId\n                            |> Async.AwaitTask\n\n                        let summaryArtifactMetadata = withCorrelationId requestMetadata (addSummaryCorrelationId correlationId \"summary-artifact\")\n\n                        let summaryLinkMetadata = withCorrelationId requestMetadata (addSummaryCorrelationId correlationId \"summary-link\")\n\n                        let! summaryArtifactResult =\n                            createArtifactFromContent\n                                repositoryDto\n                                graceIds\n                                workItemId\n                                summaryArtifactMetadata\n                                ArtifactType.AgentSummary\n                                parameters.SummaryMimeType\n                                parameters.SummaryContent\n                            |> Async.AwaitTask\n\n                        match summaryArtifactResult with\n                        | Error graceError ->\n                            return!\n                                context\n                                |> result400BadRequest (graceError |> withContext)\n                                |> Async.AwaitTask\n                        | Ok summaryArtifactId ->\n                            let! summaryLinkResult =\n                                handleWorkItemCommandAllowReplay workItemActorProxy (WorkItemCommand.LinkArtifact summaryArtifactId) summaryLinkMetadata\n                                |> Async.AwaitTask\n\n                            match summaryLinkResult with\n                            | Error graceError ->\n                                return!\n                                    context\n                                    |> result400BadRequest (graceError |> withContext)\n                                    |> Async.AwaitTask\n                            | Ok _ ->\n                                let hasPromptContent =\n                                    not\n                                    <| String.IsNullOrWhiteSpace(parameters.PromptContent)\n\n                                let! promptArtifactResult =\n                                    if hasPromptContent then\n                                        async {\n                                            let promptArtifactMetadata =\n                                                withCorrelationId requestMetadata (addSummaryCorrelationId correlationId \"prompt-artifact\")\n\n                                            let promptLinkMetadata = withCorrelationId requestMetadata (addSummaryCorrelationId correlationId \"prompt-link\")\n\n                                            let! createdPromptArtifactResult =\n                                                createArtifactFromContent\n                                                    repositoryDto\n                                                    graceIds\n                                                    workItemId\n                                                    promptArtifactMetadata\n                                                    ArtifactType.Prompt\n                                                    parameters.PromptMimeType\n                                                    parameters.PromptContent\n                                                |> Async.AwaitTask\n\n                                            match createdPromptArtifactResult with\n                                            | Error graceError -> return Error graceError\n                                            | Ok promptArtifactId ->\n                                                let! promptLinkResult =\n                                                    handleWorkItemCommandAllowReplay\n                                                        workItemActorProxy\n                                                        (WorkItemCommand.LinkArtifact promptArtifactId)\n                                                        promptLinkMetadata\n                                                    |> Async.AwaitTask\n\n                                                match promptLinkResult with\n                                                | Error graceError -> return Error graceError\n                                                | Ok _ -> return Ok(Some promptArtifactId)\n                                        }\n                                        |> Async.StartAsTask\n                                        |> Async.AwaitTask\n                                    else\n                                        async { return Ok None }\n\n                                match promptArtifactResult with\n                                | Error graceError ->\n                                    return!\n                                        context\n                                        |> result400BadRequest (graceError |> withContext)\n                                        |> Async.AwaitTask\n                                | Ok promptArtifactId ->\n                                    let promotionSetIdOption = tryParseNonEmptyGuid parameters.PromotionSetId\n\n                                    let! promotionSetLinkResult =\n                                        match promotionSetIdOption with\n                                        | Some promotionSetId ->\n                                            let promotionSetLinkMetadata =\n                                                withCorrelationId requestMetadata (addSummaryCorrelationId correlationId \"promotion-set-link\")\n\n                                            handleWorkItemCommandAllowReplay\n                                                workItemActorProxy\n                                                (WorkItemCommand.LinkPromotionSet promotionSetId)\n                                                promotionSetLinkMetadata\n                                            |> Async.AwaitTask\n                                        | None -> async { return Ok() }\n\n                                    match promotionSetLinkResult with\n                                    | Error graceError ->\n                                        return!\n                                            context\n                                            |> result400BadRequest (graceError |> withContext)\n                                            |> Async.AwaitTask\n                                    | Ok _ ->\n                                        let response =\n                                            AddSummaryResult(\n                                                WorkItemId = workItemId.ToString(),\n                                                SummaryArtifactId = summaryArtifactId.ToString(),\n                                                PromptArtifactId =\n                                                    (promptArtifactId\n                                                     |> Option.map (fun value -> value.ToString())\n                                                     |> Option.defaultValue String.Empty),\n                                                PromotionSetId =\n                                                    (promotionSetIdOption\n                                                     |> Option.map (fun value -> value.ToString())\n                                                     |> Option.defaultValue String.Empty)\n                                            )\n\n                                        let graceReturnValue =\n                                            (GraceReturnValue.Create response correlationId)\n                                                .enhance(parameterDictionary)\n                                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                                .enhance(nameof WorkItemId, workItemId)\n                                                .enhance(\"Command\", \"AddSummary\")\n                                                .enhance (\"Path\", context.Request.Path.Value)\n\n                                        return!\n                                            context\n                                            |> result200Ok graceReturnValue\n                                            |> Async.AwaitTask\n            }\n            |> Async.StartAsTask\n\n    /// Creates a new work item.\n    let Create: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"Create\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<CreateWorkItemParameters>\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validations =\n                    [|\n                        Guid.isValidAndNotEmptyGuid parameters.WorkItemId WorkItemError.InvalidWorkItemId\n                    |]\n\n                let! validationsPassed = validations |> allPass\n\n                if validationsPassed then\n                    let workItemId = Guid.Parse(parameters.WorkItemId)\n                    let metadata = createMetadata context\n                    let parameterDictionary = getParametersAsDictionary parameters\n\n                    let! createResult =\n                        withWorkItemNumberLock graceIds.RepositoryId correlationId (fun () ->\n                            task {\n                                let workItemNumberCounterActorProxy = WorkItemNumberCounter.CreateActorProxy graceIds.RepositoryId correlationId\n                                let! workItemNumber = workItemNumberCounterActorProxy.AllocateNext correlationId\n                                let actorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n\n                                let command =\n                                    WorkItemCommand.Create(\n                                        workItemId,\n                                        workItemNumber,\n                                        Guid.Parse(parameters.OwnerId),\n                                        Guid.Parse(parameters.OrganizationId),\n                                        Guid.Parse(parameters.RepositoryId),\n                                        parameters.Title,\n                                        parameters.Description\n                                    )\n\n                                match! actorProxy.Handle command metadata with\n                                | Ok graceReturnValue ->\n                                    do! cacheWorkItemNumber graceIds.RepositoryId workItemNumber workItemId correlationId\n                                    return Ok graceReturnValue\n                                | Error graceError -> return Error graceError\n                            })\n\n                    match createResult with\n                    | Ok graceReturnValue ->\n                        graceReturnValue\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof WorkItemId, workItemId)\n                            .enhance(\"Command\", nameof Create)\n                            .enhance (\"Path\", context.Request.Path.Value)\n                        |> ignore\n\n                        return! context |> result200Ok graceReturnValue\n                    | Error graceError ->\n                        graceError\n                            .enhance(parameterDictionary)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance(nameof WorkItemId, workItemId)\n                            .enhance(\"Command\", nameof Create)\n                            .enhance (\"Path\", context.Request.Path.Value)\n                        |> ignore\n\n                        return! context |> result400BadRequest graceError\n                else\n                    let! error = validations |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create errorMessage correlationId)\n            }\n\n    /// Gets a work item.\n    let Get: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n\n                let validations (parameters: GetWorkItemParameters) =\n                    [|\n                        validateWorkItemIdentifier parameters.WorkItemId\n                    |]\n\n                let query (context: HttpContext) _ (actorProxy: IWorkItemActor) = actorProxy.Get(getCorrelationId context)\n\n                let! parameters = context |> parse<GetWorkItemParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                context.Items[ \"Command\" ] <- \"Get\"\n                return! processQuery context parameters validations query\n            }\n\n    type private WorkItemAttachment = { ArtifactId: ArtifactId; Metadata: ArtifactMetadata; AttachmentType: string }\n\n    let private getAttachmentTypeName (artifactType: ArtifactType) =\n        if isNull (box artifactType) then\n            \"summary\"\n        else\n            match artifactType with\n            | ArtifactType.AgentSummary -> \"summary\"\n            | ArtifactType.Prompt -> \"prompt\"\n            | ArtifactType.ReviewNotes -> \"notes\"\n            | ArtifactType.Other kind when\n                kind.Equals(\"summary\", StringComparison.OrdinalIgnoreCase)\n                || kind.Equals(\"agentsummary\", StringComparison.OrdinalIgnoreCase)\n                ->\n                \"summary\"\n            | ArtifactType.Other kind when kind.Equals(\"prompt\", StringComparison.OrdinalIgnoreCase) -> \"prompt\"\n            | ArtifactType.Other kind when\n                kind.Equals(\"notes\", StringComparison.OrdinalIgnoreCase)\n                || kind.Equals(\"reviewnotes\", StringComparison.OrdinalIgnoreCase)\n                ->\n                \"notes\"\n            | _ -> \"other\"\n\n    let private tryGetReviewerAttachmentTypeName (artifactType: ArtifactType) =\n        if isNull (box artifactType) then\n            Some \"summary\"\n        else\n            match artifactType with\n            | ArtifactType.AgentSummary -> Some \"summary\"\n            | ArtifactType.Prompt -> Some \"prompt\"\n            | ArtifactType.ReviewNotes -> Some \"notes\"\n            | ArtifactType.Other kind when\n                kind.Equals(\"summary\", StringComparison.OrdinalIgnoreCase)\n                || kind.Equals(\"agentsummary\", StringComparison.OrdinalIgnoreCase)\n                ->\n                Some \"summary\"\n            | ArtifactType.Other kind when kind.Equals(\"prompt\", StringComparison.OrdinalIgnoreCase) -> Some \"prompt\"\n            | ArtifactType.Other kind when\n                kind.Equals(\"notes\", StringComparison.OrdinalIgnoreCase)\n                || kind.Equals(\"reviewnotes\", StringComparison.OrdinalIgnoreCase)\n                ->\n                Some \"notes\"\n            | _ -> None\n\n    let private isTextMimeType (mimeType: string) =\n        if String.IsNullOrWhiteSpace(mimeType) then\n            false\n        else\n            let normalized = mimeType.Trim().ToLowerInvariant()\n\n            normalized.StartsWith(\"text/\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Contains(\"+json\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Contains(\"+xml\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/json\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/xml\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/yaml\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/x-yaml\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/toml\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/javascript\", StringComparison.OrdinalIgnoreCase)\n            || normalized.Equals(\"application/x-javascript\", StringComparison.OrdinalIgnoreCase)\n\n    let private toAttachmentDescriptor (attachment: WorkItemAttachment) =\n        WorkItemAttachmentDescriptor(\n            ArtifactId = attachment.ArtifactId.ToString(),\n            AttachmentType = attachment.AttachmentType,\n            MimeType = attachment.Metadata.MimeType,\n            Size = attachment.Metadata.Size,\n            CreatedAt = attachment.Metadata.CreatedAt.ToString()\n        )\n\n    let private selectAttachmentDeterministically (attachments: WorkItemAttachment list) (latest: bool) =\n        if List.isEmpty attachments then\n            None\n        else\n            let ordered = attachments |> List.toArray\n\n            if latest then Some ordered[ordered.Length - 1] else Some ordered[0]\n\n    let private fetchLinkedReviewerAttachments (repositoryId: RepositoryId) (correlationId: CorrelationId) (workItemDto: WorkItemDto) =\n        task {\n            let attachments = ResizeArray<WorkItemAttachment>()\n            let artifactIds = workItemDto.ArtifactIds |> List.toArray\n            let mutable index = 0\n\n            while index < artifactIds.Length do\n                let artifactId = artifactIds[index]\n                let artifactActorProxy = Artifact.CreateActorProxy artifactId repositoryId correlationId\n                let! artifactMetadata = artifactActorProxy.Get correlationId\n\n                match artifactMetadata with\n                | Some metadata ->\n                    match tryGetReviewerAttachmentTypeName metadata.ArtifactType with\n                    | Some attachmentType -> attachments.Add({ ArtifactId = artifactId; Metadata = metadata; AttachmentType = attachmentType })\n                    | None -> ()\n                | None -> ()\n\n                index <- index + 1\n\n            return\n                attachments\n                |> Seq.sortBy (fun attachment -> attachment.Metadata.CreatedAt, attachment.ArtifactId.ToString(\"N\"))\n                |> Seq.toList\n        }\n\n    let private downloadArtifactContentBytes repositoryDto (blobPath: string) (correlationId: CorrelationId) =\n        task {\n            try\n                let! containerClient = getContainerClient repositoryDto correlationId\n                let blobClient = containerClient.GetBlobClient(blobPath)\n                let! exists = blobClient.ExistsAsync()\n\n                if not exists.Value then\n                    if isGraceTestingEnabled () then\n                        return Ok(Array.empty<byte>)\n                    else\n                        return Error(GraceError.Create $\"Artifact content was not found at blob path '{blobPath}'.\" correlationId)\n                else\n                    let! downloadResult = blobClient.DownloadContentAsync()\n                    return Ok(downloadResult.Value.Content.ToArray())\n            with\n            | :? Azure.RequestFailedException as requestEx when\n                isGraceTestingEnabled ()\n                && (String.Equals(requestEx.ErrorCode, \"BlobNotFound\", StringComparison.OrdinalIgnoreCase)\n                    || String.Equals(requestEx.ErrorCode, \"ContainerNotFound\", StringComparison.OrdinalIgnoreCase))\n                ->\n                return Ok(Array.empty<byte>)\n            | ex -> return Error(GraceError.Create $\"Failed to download artifact content: {ex.Message}\" correlationId)\n        }\n\n    let private tryReadAttachmentContent\n        (organizationId: OrganizationId)\n        (repositoryId: RepositoryId)\n        (correlationId: CorrelationId)\n        (attachment: WorkItemAttachment)\n        =\n        task {\n            if isTextMimeType attachment.Metadata.MimeType then\n                let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryId correlationId\n                let! repositoryDto = repositoryActorProxy.Get correlationId\n                let! bytesResult = downloadArtifactContentBytes repositoryDto attachment.Metadata.BlobPath correlationId\n\n                match bytesResult with\n                | Ok bytes -> return Ok(Encoding.UTF8.GetString(bytes))\n                | Error graceError -> return Error graceError\n            else\n                return Ok String.Empty\n        }\n\n    let private completeShowAttachmentRequest\n        (context: HttpContext)\n        (ownerId: OwnerId)\n        (organizationId: OrganizationId)\n        (repositoryId: RepositoryId)\n        (correlationId: CorrelationId)\n        (parameterDictionary: IReadOnlyDictionary<string, obj>)\n        (withContext: GraceError -> GraceError)\n        (workItemId: WorkItemId)\n        (workItemNumber: WorkItemNumber)\n        (selectedAttachment: WorkItemAttachment)\n        (availableAttachmentCount: int)\n        (selectedUsingLatest: bool)\n        =\n        task {\n            let isTextContent = isTextMimeType selectedAttachment.Metadata.MimeType\n            let! contentResult = tryReadAttachmentContent organizationId repositoryId correlationId selectedAttachment\n\n            match contentResult with\n            | Error graceError ->\n                return!\n                    context\n                    |> result400BadRequest (graceError |> withContext)\n            | Ok content ->\n                let response =\n                    ShowWorkItemAttachmentResult(\n                        WorkItemId = workItemId.ToString(),\n                        WorkItemNumber = workItemNumber,\n                        AttachmentType = selectedAttachment.AttachmentType,\n                        ArtifactId = selectedAttachment.ArtifactId.ToString(),\n                        MimeType = selectedAttachment.Metadata.MimeType,\n                        Size = selectedAttachment.Metadata.Size,\n                        CreatedAt = selectedAttachment.Metadata.CreatedAt.ToString(),\n                        IsTextContent = isTextContent,\n                        Content = content,\n                        AvailableAttachmentCount = availableAttachmentCount,\n                        SelectedUsingLatest = selectedUsingLatest\n                    )\n\n                let graceReturnValue =\n                    (GraceReturnValue.Create response correlationId)\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, ownerId)\n                        .enhance(nameof OrganizationId, organizationId)\n                        .enhance(nameof RepositoryId, repositoryId)\n                        .enhance(nameof WorkItemId, workItemId)\n                        .enhance(\"Command\", \"ShowAttachment\")\n                        .enhance (\"Path\", context.Request.Path.Value)\n\n                return! context |> result200Ok graceReturnValue\n        }\n\n    let private buildLinksDto (workItemDto: WorkItemDto) (artifactMetadataById: IReadOnlyDictionary<ArtifactId, ArtifactMetadata option>) =\n        let agentSummaryArtifactIds = ResizeArray<ArtifactId>()\n        let promptArtifactIds = ResizeArray<ArtifactId>()\n        let reviewNotesArtifactIds = ResizeArray<ArtifactId>()\n        let otherArtifactIds = ResizeArray<ArtifactId>()\n\n        workItemDto.ArtifactIds\n        |> Seq.iter (fun artifactId ->\n            match artifactMetadataById[artifactId] with\n            | Some artifactMetadata ->\n                match artifactMetadata.ArtifactType with\n                | ArtifactType.AgentSummary -> agentSummaryArtifactIds.Add(artifactId)\n                | ArtifactType.Prompt -> promptArtifactIds.Add(artifactId)\n                | ArtifactType.ReviewNotes -> reviewNotesArtifactIds.Add(artifactId)\n                | _ -> otherArtifactIds.Add(artifactId)\n            | None -> otherArtifactIds.Add(artifactId))\n\n        {\n            WorkItemId = workItemDto.WorkItemId\n            WorkItemNumber = workItemDto.WorkItemNumber\n            ReferenceIds = workItemDto.ReferenceIds\n            PromotionSetIds = workItemDto.PromotionSetIds\n            ArtifactIds = workItemDto.ArtifactIds\n            AgentSummaryArtifactIds = agentSummaryArtifactIds |> Seq.toList\n            PromptArtifactIds = promptArtifactIds |> Seq.toList\n            ReviewNotesArtifactIds = reviewNotesArtifactIds |> Seq.toList\n            OtherArtifactIds = otherArtifactIds |> Seq.toList\n        }\n\n    /// Gets work item links grouped by link category.\n    let GetLinks: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"GetLinks\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<GetWorkItemLinksParameters>\n                let parameterDictionary = getParametersAsDictionary parameters\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validationResults =\n                    [|\n                        validateWorkItemIdentifier parameters.WorkItemId\n                    |]\n\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match! resolveWorkItemId graceIds.RepositoryId parameters.WorkItemId correlationId with\n                    | Error graceError -> return! context |> result400BadRequest graceError\n                    | Ok workItemId ->\n                        let workItemActorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n                        let! workItemDto = workItemActorProxy.Get correlationId\n                        do! cacheWorkItemNumber graceIds.RepositoryId workItemDto.WorkItemNumber workItemDto.WorkItemId correlationId\n\n                        let artifactMetadataById = Dictionary<ArtifactId, ArtifactMetadata option>()\n                        let artifactIds = workItemDto.ArtifactIds |> List.toArray\n                        let mutable i = 0\n\n                        while i < artifactIds.Length do\n                            let artifactId = artifactIds[i]\n                            let artifactActorProxy = Artifact.CreateActorProxy artifactId graceIds.RepositoryId correlationId\n                            let! artifactMetadata = artifactActorProxy.Get correlationId\n                            artifactMetadataById[artifactId] <- artifactMetadata\n                            i <- i + 1\n\n                        let linksDto = buildLinksDto workItemDto artifactMetadataById\n\n                        let graceReturnValue =\n                            (GraceReturnValue.Create linksDto correlationId)\n                                .enhance(parameterDictionary)\n                                .enhance(nameof OwnerId, graceIds.OwnerId)\n                                .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                .enhance(nameof WorkItemId, workItemId)\n                                .enhance (\"Path\", context.Request.Path.Value)\n\n                        return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create errorMessage correlationId)\n            }\n\n    /// Lists reviewer attachments linked to a work item.\n    let ListAttachments: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"ListAttachments\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<ListWorkItemAttachmentsParameters>\n\n                let parameterDictionary = getParametersAsDictionary parameters\n                let ownerId, organizationId, repositoryId = resolveWorkItemScopeIds graceIds parameters\n\n                parameters.OwnerId <- $\"{ownerId}\"\n                parameters.OrganizationId <- $\"{organizationId}\"\n                parameters.RepositoryId <- $\"{repositoryId}\"\n\n                let withContext (graceError: GraceError) =\n                    graceError\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, ownerId)\n                        .enhance(nameof OrganizationId, organizationId)\n                        .enhance(nameof RepositoryId, repositoryId)\n                        .enhance(\"Command\", \"ListAttachments\")\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    graceError\n\n                try\n                    let validationResults = validateListWorkItemAttachmentsParameters parameters\n                    let! validationsPassed = validationResults |> allPass\n\n                    if validationsPassed then\n                        match! resolveWorkItemId repositoryId parameters.WorkItemId correlationId with\n                        | Error graceError ->\n                            return!\n                                context\n                                |> result400BadRequest (graceError |> withContext)\n                        | Ok workItemId ->\n                            let workItemActorProxy = WorkItem.CreateActorProxy workItemId repositoryId correlationId\n                            let! workItemDto = workItemActorProxy.Get correlationId\n                            do! cacheWorkItemNumber repositoryId workItemDto.WorkItemNumber workItemDto.WorkItemId correlationId\n                            let! attachments = fetchLinkedReviewerAttachments repositoryId correlationId workItemDto\n\n                            let response =\n                                ListWorkItemAttachmentsResult(\n                                    WorkItemId = workItemId.ToString(),\n                                    WorkItemNumber = workItemDto.WorkItemNumber,\n                                    Attachments = List<WorkItemAttachmentDescriptor>(attachments |> Seq.map toAttachmentDescriptor)\n                                )\n\n                            let graceReturnValue =\n                                (GraceReturnValue.Create response correlationId)\n                                    .enhance(parameterDictionary)\n                                    .enhance(nameof OwnerId, ownerId)\n                                    .enhance(nameof OrganizationId, organizationId)\n                                    .enhance(nameof RepositoryId, repositoryId)\n                                    .enhance(nameof WorkItemId, workItemId)\n                                    .enhance(\"Command\", \"ListAttachments\")\n                                    .enhance (\"Path\", context.Request.Path.Value)\n\n                            return! context |> result200Ok graceReturnValue\n                    else\n                        let! error = validationResults |> getFirstError\n                        let errorMessage = WorkItemError.getErrorMessage error\n\n                        return!\n                            context\n                            |> result400BadRequest (\n                                GraceError.Create errorMessage correlationId\n                                |> withContext\n                            )\n                with\n                | ex ->\n                    return!\n                        context\n                        |> result500ServerError (\n                            GraceError.CreateWithException ex String.Empty correlationId\n                            |> withContext\n                        )\n            }\n\n    /// Shows a reviewer attachment for a work item using deterministic type-filtered selection.\n    let ShowAttachment: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"ShowAttachment\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n                let! parameters = context |> parse<ShowWorkItemAttachmentParameters>\n                let parameterDictionary = getParametersAsDictionary parameters\n                let ownerId, organizationId, repositoryId = resolveWorkItemScopeIds graceIds parameters\n\n                parameters.OwnerId <- $\"{ownerId}\"\n                parameters.OrganizationId <- $\"{organizationId}\"\n                parameters.RepositoryId <- $\"{repositoryId}\"\n\n                let withContext (graceError: GraceError) =\n                    graceError\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, ownerId)\n                        .enhance(nameof OrganizationId, organizationId)\n                        .enhance(nameof RepositoryId, repositoryId)\n                        .enhance(\"Command\", \"ShowAttachment\")\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    graceError\n\n                let validationResults = validateShowWorkItemAttachmentParameters parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match parseAttachmentType parameters.AttachmentType with\n                    | Error error ->\n                        let errorMessage = WorkItemError.getErrorMessage error\n\n                        return!\n                            context\n                            |> result400BadRequest (\n                                GraceError.Create errorMessage correlationId\n                                |> withContext\n                            )\n                    | Ok artifactType ->\n                        let requestedAttachmentType = getAttachmentTypeName artifactType\n\n                        match! resolveWorkItemId repositoryId parameters.WorkItemId correlationId with\n                        | Error graceError ->\n                            return!\n                                context\n                                |> result400BadRequest (graceError |> withContext)\n                        | Ok workItemId ->\n                            let workItemActorProxy = WorkItem.CreateActorProxy workItemId repositoryId correlationId\n                            let! workItemDto = workItemActorProxy.Get correlationId\n                            do! cacheWorkItemNumber repositoryId workItemDto.WorkItemNumber workItemDto.WorkItemId correlationId\n                            let! attachments = fetchLinkedReviewerAttachments repositoryId correlationId workItemDto\n\n                            let filteredAttachments =\n                                attachments\n                                |> List.filter (fun attachment -> attachment.AttachmentType.Equals(requestedAttachmentType, StringComparison.OrdinalIgnoreCase))\n\n                            match selectAttachmentDeterministically filteredAttachments parameters.Latest with\n                            | None ->\n                                return!\n                                    context\n                                    |> result400BadRequest (\n                                        GraceError.Create $\"No '{requestedAttachmentType}' attachments are linked to this work item.\" correlationId\n                                        |> withContext\n                                    )\n                            | Some selectedAttachment ->\n                                return!\n                                    completeShowAttachmentRequest\n                                        context\n                                        ownerId\n                                        organizationId\n                                        repositoryId\n                                        correlationId\n                                        parameterDictionary\n                                        withContext\n                                        workItemId\n                                        workItemDto.WorkItemNumber\n                                        selectedAttachment\n                                        filteredAttachments.Length\n                                        parameters.Latest\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    return!\n                        context\n                        |> result400BadRequest (\n                            GraceError.Create errorMessage correlationId\n                            |> withContext\n                        )\n            }\n\n    /// Gets download metadata for a linked reviewer attachment.\n    let DownloadAttachment: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"DownloadAttachment\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<DownloadWorkItemAttachmentParameters>\n\n                let parameterDictionary = getParametersAsDictionary parameters\n                let ownerId, organizationId, repositoryId = resolveWorkItemScopeIds graceIds parameters\n\n                parameters.OwnerId <- $\"{ownerId}\"\n                parameters.OrganizationId <- $\"{organizationId}\"\n                parameters.RepositoryId <- $\"{repositoryId}\"\n\n                let withContext (graceError: GraceError) =\n                    graceError\n                        .enhance(parameterDictionary)\n                        .enhance(nameof OwnerId, ownerId)\n                        .enhance(nameof OrganizationId, organizationId)\n                        .enhance(nameof RepositoryId, repositoryId)\n                        .enhance(\"Command\", \"DownloadAttachment\")\n                        .enhance (\"Path\", context.Request.Path.Value)\n                    |> ignore\n\n                    graceError\n\n                let validationResults = validateDownloadWorkItemAttachmentParameters parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match! resolveWorkItemId repositoryId parameters.WorkItemId correlationId with\n                    | Error graceError ->\n                        return!\n                            context\n                            |> result400BadRequest (graceError |> withContext)\n                    | Ok workItemId ->\n                        let workItemActorProxy = WorkItem.CreateActorProxy workItemId repositoryId correlationId\n                        let! workItemDto = workItemActorProxy.Get correlationId\n                        do! cacheWorkItemNumber repositoryId workItemDto.WorkItemNumber workItemDto.WorkItemId correlationId\n                        let artifactId = Guid.Parse(parameters.ArtifactId)\n\n                        let isLinked =\n                            workItemDto.ArtifactIds\n                            |> List.contains artifactId\n\n                        if not isLinked then\n                            return!\n                                context\n                                |> result400BadRequest (\n                                    GraceError.Create \"The specified artifact is not linked to the work item.\" correlationId\n                                    |> withContext\n                                )\n                        else\n                            let artifactActorProxy = Artifact.CreateActorProxy artifactId repositoryId correlationId\n                            let! artifactMetadata = artifactActorProxy.Get correlationId\n\n                            match artifactMetadata with\n                            | None ->\n                                return!\n                                    context\n                                    |> result400BadRequest (\n                                        GraceError.Create (ArtifactError.getErrorMessage ArtifactError.ArtifactDoesNotExist) correlationId\n                                        |> withContext\n                                    )\n                            | Some metadata ->\n                                match tryGetReviewerAttachmentTypeName metadata.ArtifactType with\n                                | None ->\n                                    return!\n                                        context\n                                        |> result400BadRequest (\n                                            GraceError.Create (WorkItemError.getErrorMessage WorkItemError.InvalidArtifactType) correlationId\n                                            |> withContext\n                                        )\n                                | Some attachmentType ->\n                                    let repositoryActorProxy = Repository.CreateActorProxy organizationId repositoryId correlationId\n\n                                    let! repositoryDto = repositoryActorProxy.Get correlationId\n                                    let! downloadUri = getUriWithReadSharedAccessSignature repositoryDto metadata.BlobPath correlationId\n\n                                    let response =\n                                        DownloadWorkItemAttachmentResult(\n                                            WorkItemId = workItemId.ToString(),\n                                            WorkItemNumber = workItemDto.WorkItemNumber,\n                                            AttachmentType = attachmentType,\n                                            ArtifactId = artifactId.ToString(),\n                                            MimeType = metadata.MimeType,\n                                            Size = metadata.Size,\n                                            CreatedAt = metadata.CreatedAt.ToString(),\n                                            DownloadUri = $\"{downloadUri}\"\n                                        )\n\n                                    let graceReturnValue =\n                                        (GraceReturnValue.Create response correlationId)\n                                            .enhance(parameterDictionary)\n                                            .enhance(nameof OwnerId, ownerId)\n                                            .enhance(nameof OrganizationId, organizationId)\n                                            .enhance(nameof RepositoryId, repositoryId)\n                                            .enhance(nameof WorkItemId, workItemId)\n                                            .enhance(nameof ArtifactId, artifactId)\n                                            .enhance(\"Command\", \"DownloadAttachment\")\n                                            .enhance (\"Path\", context.Request.Path.Value)\n\n                                    return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    return!\n                        context\n                        |> result400BadRequest (\n                            GraceError.Create errorMessage correlationId\n                            |> withContext\n                        )\n            }\n\n    /// Updates a work item.\n    let Update: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let validations (parameters: UpdateWorkItemParameters) =\n                    [|\n                        validateWorkItemIdentifier parameters.WorkItemId\n                        (if String.IsNullOrEmpty(parameters.Status) then\n                             Ok() |> returnValueTask\n                         else\n                             DiscriminatedUnion.isMemberOf<WorkItemStatus, WorkItemError> parameters.Status WorkItemError.InvalidStatus)\n                    |]\n\n                let! parameters = context |> parse<UpdateWorkItemParameters>\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n                let parameterDictionary = getParametersAsDictionary parameters\n\n                let validationResults = validations parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match! resolveWorkItemId graceIds.RepositoryId parameters.WorkItemId correlationId with\n                    | Error graceError -> return! context |> result400BadRequest graceError\n                    | Ok workItemId ->\n                        let actorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n                        let metadata = createMetadata context\n                        let commands = buildUpdateCommands parameters\n\n                        if commands.IsEmpty then\n                            let graceError =\n                                (GraceError.Create \"No updates were provided.\" correlationId)\n                                    .enhance(nameof OwnerId, graceIds.OwnerId)\n                                    .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                    .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                    .enhance(nameof WorkItemId, workItemId)\n                                    .enhance (\"Path\", context.Request.Path.Value)\n\n                            return! context |> result400BadRequest graceError\n                        else\n                            let commandsArray = commands |> List.toArray\n                            let mutable result: GraceResult<string> option = None\n                            let mutable i = 0\n\n                            while i < commandsArray.Length do\n                                match result with\n                                | Some (Error _) -> i <- commandsArray.Length\n                                | _ ->\n                                    let! handleResult = actorProxy.Handle commandsArray[i] metadata\n                                    result <- Some handleResult\n                                    i <- i + 1\n\n                            match result with\n                            | Some (Ok graceReturnValue) ->\n                                graceReturnValue\n                                    .enhance(parameterDictionary)\n                                    .enhance(nameof OwnerId, graceIds.OwnerId)\n                                    .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                    .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                    .enhance(nameof WorkItemId, workItemId)\n                                    .enhance(\"Command\", \"Update\")\n                                    .enhance (\"Path\", context.Request.Path.Value)\n                                |> ignore\n\n                                return! context |> result200Ok graceReturnValue\n                            | Some (Error graceError) ->\n                                graceError\n                                    .enhance(parameterDictionary)\n                                    .enhance(nameof OwnerId, graceIds.OwnerId)\n                                    .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                                    .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                                    .enhance(nameof WorkItemId, workItemId)\n                                    .enhance(\"Command\", \"Update\")\n                                    .enhance (\"Path\", context.Request.Path.Value)\n                                |> ignore\n\n                                return! context |> result400BadRequest graceError\n                            | None ->\n                                return!\n                                    context\n                                    |> result400BadRequest (GraceError.Create \"No updates were applied.\" correlationId)\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    let graceError =\n                        (GraceError.Create errorMessage correlationId)\n                            .enhance(nameof OwnerId, graceIds.OwnerId)\n                            .enhance(nameof OrganizationId, graceIds.OrganizationId)\n                            .enhance(nameof RepositoryId, graceIds.RepositoryId)\n                            .enhance (\"Path\", context.Request.Path.Value)\n\n                    return! context |> result400BadRequest graceError\n            }\n\n    /// Links a reference to a work item.\n    let LinkReference: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: LinkReferenceParameters) = validateLinkReferenceParameters parameters\n\n                let command (parameters: LinkReferenceParameters) =\n                    WorkItemCommand.LinkReference(Guid.Parse(parameters.ReferenceId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof LinkReference\n                return! processCommand context validations command\n            }\n\n    /// Removes a reference link from a work item.\n    let RemoveReferenceLink: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: RemoveReferenceLinkParameters) =\n                    [|\n                        validateWorkItemIdentifier parameters.WorkItemId\n                        Guid.isValidAndNotEmptyGuid parameters.ReferenceId WorkItemError.InvalidReferenceId\n                    |]\n\n                let command (parameters: RemoveReferenceLinkParameters) =\n                    WorkItemCommand.UnlinkReference(Guid.Parse(parameters.ReferenceId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- \"RemoveReferenceLink\"\n                return! processCommand context validations command\n            }\n\n    /// Links a promotion set to a work item.\n    let LinkPromotionSet: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: LinkPromotionSetParameters) = validateLinkPromotionSetParameters parameters\n\n                let command (parameters: LinkPromotionSetParameters) =\n                    WorkItemCommand.LinkPromotionSet(Guid.Parse(parameters.PromotionSetId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof LinkPromotionSet\n                return! processCommand context validations command\n            }\n\n    /// Removes a promotion set link from a work item.\n    let RemovePromotionSetLink: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: RemovePromotionSetLinkParameters) =\n                    [|\n                        validateWorkItemIdentifier parameters.WorkItemId\n                        Guid.isValidAndNotEmptyGuid parameters.PromotionSetId WorkItemError.InvalidPromotionSetId\n                    |]\n\n                let command (parameters: RemovePromotionSetLinkParameters) =\n                    WorkItemCommand.UnlinkPromotionSet(Guid.Parse(parameters.PromotionSetId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- \"RemovePromotionSetLink\"\n                return! processCommand context validations command\n            }\n\n    /// Links an artifact to a work item.\n    let LinkArtifact: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: LinkArtifactParameters) = validateLinkArtifactParameters parameters\n\n                let command (parameters: LinkArtifactParameters) =\n                    WorkItemCommand.LinkArtifact(Guid.Parse(parameters.ArtifactId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- nameof LinkArtifact\n                return! processCommand context validations command\n            }\n\n    /// Removes an artifact link from a work item.\n    let RemoveArtifactLink: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                let validations (parameters: RemoveArtifactLinkParameters) =\n                    [|\n                        validateWorkItemIdentifier parameters.WorkItemId\n                        Guid.isValidAndNotEmptyGuid parameters.ArtifactId WorkItemError.InvalidArtifactId\n                    |]\n\n                let command (parameters: RemoveArtifactLinkParameters) =\n                    WorkItemCommand.UnlinkArtifact(Guid.Parse(parameters.ArtifactId))\n                    |> returnValueTask\n\n                context.Items[ \"Command\" ] <- \"RemoveArtifactLink\"\n                return! processCommand context validations command\n            }\n\n    /// Removes artifact links from a work item by artifact type.\n    let RemoveArtifactTypeLinks: HttpHandler =\n        fun (_next: HttpFunc) (context: HttpContext) ->\n            task {\n                use activity = activitySource.StartActivity(\"RemoveArtifactTypeLinks\", ActivityKind.Server)\n                let graceIds = getGraceIds context\n                let correlationId = getCorrelationId context\n\n                let! parameters =\n                    context\n                    |> parse<RemoveArtifactTypeLinksParameters>\n\n                parameters.OwnerId <- graceIds.OwnerIdString\n                parameters.OrganizationId <- graceIds.OrganizationIdString\n                parameters.RepositoryId <- graceIds.RepositoryIdString\n\n                let validationResults = validateRemoveArtifactTypeLinksParameters parameters\n                let! validationsPassed = validationResults |> allPass\n\n                if validationsPassed then\n                    match parseRemovableArtifactType parameters.ArtifactType with\n                    | Error error ->\n                        let errorMessage = WorkItemError.getErrorMessage error\n\n                        return!\n                            context\n                            |> result400BadRequest (GraceError.Create errorMessage correlationId)\n                    | Ok artifactType ->\n                        match! resolveWorkItemId graceIds.RepositoryId parameters.WorkItemId correlationId with\n                        | Error graceError -> return! context |> result400BadRequest graceError\n                        | Ok workItemId ->\n                            let workItemActorProxy = WorkItem.CreateActorProxy workItemId graceIds.RepositoryId correlationId\n                            let! workItemDto = workItemActorProxy.Get correlationId\n                            let artifactIds = workItemDto.ArtifactIds |> List.toArray\n                            let removableArtifactIds = ResizeArray<ArtifactId>()\n                            let mutable i = 0\n\n                            while i < artifactIds.Length do\n                                let artifactId = artifactIds[i]\n                                let artifactActorProxy = Artifact.CreateActorProxy artifactId graceIds.RepositoryId correlationId\n                                let! artifactMetadata = artifactActorProxy.Get correlationId\n\n                                match artifactMetadata with\n                                | Some metadata when metadata.ArtifactType = artifactType -> removableArtifactIds.Add(artifactId)\n                                | _ -> ()\n\n                                i <- i + 1\n\n                            let metadata = createMetadata context\n                            let removableArtifactIdsArray = removableArtifactIds |> Seq.toArray\n                            let mutable removedCount = 0\n                            let mutable removeError: GraceError option = None\n                            let mutable j = 0\n\n                            while j < removableArtifactIdsArray.Length\n                                  && removeError.IsNone do\n                                let artifactId = removableArtifactIdsArray[j]\n                                let! removeResult = workItemActorProxy.Handle (WorkItemCommand.UnlinkArtifact artifactId) metadata\n\n                                match removeResult with\n                                | Ok _ ->\n                                    removedCount <- removedCount + 1\n                                    j <- j + 1\n                                | Error graceError -> removeError <- Some graceError\n\n                            match removeError with\n                            | Some graceError -> return! context |> result400BadRequest graceError\n                            | None ->\n                                let resultMessage = $\"Removed {removedCount} artifact link(s) of type {getDiscriminatedUnionCaseName artifactType}.\"\n\n                                let graceReturnValue = GraceReturnValue.Create resultMessage correlationId\n                                return! context |> result200Ok graceReturnValue\n                else\n                    let! error = validationResults |> getFirstError\n                    let errorMessage = WorkItemError.getErrorMessage error\n\n                    return!\n                        context\n                        |> result400BadRequest (GraceError.Create errorMessage correlationId)\n            }\n"
  },
  {
    "path": "src/Grace.Server/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "src/Grace.Server/web.config",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<configuration>\n</configuration>\n"
  },
  {
    "path": "src/Grace.Server.Tests/AGENTS.md",
    "content": "# Grace.Server.Tests Agents Guide\n\nGlobal policies live in `../AGENTS.md`; follow them before touching tests here.\n\n## Purpose\n\n- End-to-end integration tests that boot the local Aspire stack (Cosmos + Azurite + Service Bus emulator + Redis + Grace.Server).\n- Verify real HTTP flows against `Grace.Server` with an initialized Service Bus test subscription.\n- Validate actor behavior through server endpoints/contracts wherever possible.\n\n## Test File Organization\n\n- Match server test files to server implementation files when practical:\n  - `Grace.Server/Access.Server.fs` -> `Grace.Server.Tests/Access.Server.Tests.fs`\n  - `Grace.Server/Eventing.Server.fs` -> `Grace.Server.Tests/Eventing.Server.Tests.fs`\n  - `Grace.Server/Notification.Server.fs` -> `Grace.Server.Tests/Notification.Server.Tests.fs`\n  - `Grace.Server/Queue.Server.fs` -> `Grace.Server.Tests/Queue.Server.Tests.fs`\n  - `Grace.Server/WorkItem.Server.fs` -> `Grace.Server.Tests/WorkItem.Server.Tests.fs`\n- Keep auth-focused tests separate for now.\n\n## Key Patterns\n\n- Tests use Aspire hosting via `Aspire.Hosting.Testing` and the shared host module in `AspireTestHost.fs`.\n- The first HTTP call in the test run must be `POST /owner/create` (done in the `[<SetUpFixture>]`).\n- Service Bus validation must read from the test subscription (`${serverSubscription}-tests`) to avoid racing the server processor.\n- Shared state (HttpClient, OwnerId, Service Bus settings) is set during `OneTimeSetUp` and reused by test modules.\n\n## Notes\n\n- Do not reintroduce Dapr sidecars, Dapr ports, or Dapr cleanup logic.\n- Keep diagnostics helpful: log server base address and Service Bus settings (redact secrets).\n- Set environment variable `GRACE_TEST_CLEANUP=1` to control resource cleanup after tests.\n\n## Validation\n\n- Run `dotnet build --configuration Release`.\n- Run `dotnet test --no-build --configuration Release`.\n- Run `dotnet tool run fantomas --recurse .` from `./src` after F# changes.\n"
  },
  {
    "path": "src/Grace.Server.Tests/Access.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\n\n[<NonParallelizable>]\ntype AccessBootstrapEnabledTests() =\n\n    let createOwner (client: HttpClient) =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let ownerParameters = Parameters.Owner.CreateOwnerParameters()\n            ownerParameters.OwnerId <- ownerId\n            ownerParameters.OwnerName <- $\"BootstrapOwner{Guid.NewGuid():N}\"\n            ownerParameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/owner/create\", createJsonContent ownerParameters)\n            response.EnsureSuccessStatusCode() |> ignore\n\n            return ownerId\n        }\n\n    let createGrantRoleParameters (roleId: string) (principalId: string) =\n        let parameters = Parameters.Access.GrantRoleParameters()\n        parameters.ScopeKind <- \"system\"\n        parameters.PrincipalType <- \"User\"\n        parameters.PrincipalId <- principalId\n        parameters.RoleId <- roleId\n        parameters.Source <- \"test\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createClient (userId: string) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n        client\n\n    [<Test>]\n    member _.BootstrapSeedsSystemAdminWhenConfigured() =\n        task {\n            use client = createClient testUserId\n            let! _ownerId = createOwner client\n\n            let parameters = createGrantRoleParameters \"SystemAdmin\" $\"{Guid.NewGuid()}\"\n            let! response = client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n\nnamespace Grace.Server.Tests\n\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\n\n[<NonParallelizable>]\ntype AccessBootstrapTests() =\n\n    let createOwner (client: HttpClient) =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let ownerParameters = Parameters.Owner.CreateOwnerParameters()\n            ownerParameters.OwnerId <- ownerId\n            ownerParameters.OwnerName <- $\"BootstrapOwner{Guid.NewGuid():N}\"\n            ownerParameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/owner/create\", createJsonContent ownerParameters)\n            response.EnsureSuccessStatusCode() |> ignore\n\n            return ownerId\n        }\n\n    let createGrantRoleParameters (roleId: string) (principalId: string) =\n        let parameters = Parameters.Access.GrantRoleParameters()\n        parameters.ScopeKind <- \"system\"\n        parameters.PrincipalType <- \"User\"\n        parameters.PrincipalId <- principalId\n        parameters.RoleId <- roleId\n        parameters.Source <- \"test\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createClient (userId: string) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n        client\n\n    [<Test>]\n    member _.BootstrapDisabledByDefaultReturnsForbidden() =\n        task {\n            use client = createClient $\"{Guid.NewGuid()}\"\n            let! _ownerId = createOwner client\n\n            let parameters = createGrantRoleParameters \"SystemAdmin\" $\"{Guid.NewGuid()}\"\n            let! response = client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n\nnamespace Grace.Server.Tests\n\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\n\n[<Parallelizable(ParallelScope.All)>]\ntype AccessCheckPermissionTests() =\n\n    let createClient (userId: string) (groups: string list) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n\n        if not groups.IsEmpty then\n            client.DefaultRequestHeaders.Add(\"x-grace-groups\", String.Join(\";\", groups))\n\n        client\n\n    let checkPermissionAsync (client: HttpClient) ownerId organizationId repositoryId resourceKind operation principalType principalId =\n        task {\n            let parameters = Parameters.Access.CheckPermissionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.ResourceKind <- resourceKind\n            parameters.Operation <- operation\n            parameters.Path <- \"\"\n            parameters.PrincipalType <- principalType\n            parameters.PrincipalId <- principalId\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            return! client.PostAsync(\"/access/checkPermission\", createJsonContent parameters)\n        }\n\n    [<Test>]\n    member _.SelfCheckWithoutPrincipalSpecifiedReturnsOk() =\n        task {\n            let userId = $\"{Guid.NewGuid()}\"\n            use client = createClient userId []\n\n            let! response = checkPermissionAsync client $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" \"repo\" \"RepoRead\" \"\" \"\"\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.ExplicitSelfPrincipalReturnsOk() =\n        task {\n            let userId = $\"{Guid.NewGuid()}\"\n            use client = createClient userId []\n\n            let! response = checkPermissionAsync client $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" \"repo\" \"RepoRead\" \"User\" userId\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.GroupPrincipalMemberReturnsOk() =\n        task {\n            let userId = $\"{Guid.NewGuid()}\"\n            let groupId = $\"{Guid.NewGuid()}\"\n            use client = createClient userId [ groupId ]\n\n            let! response = checkPermissionAsync client $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" \"repo\" \"RepoRead\" \"Group\" groupId\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.OtherPrincipalRequiresAdmin() =\n        task {\n            let userId = $\"{Guid.NewGuid()}\"\n            use client = createClient userId []\n\n            let! response = checkPermissionAsync client $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" $\"{Guid.NewGuid()}\" \"repo\" \"RepoRead\" \"User\" $\"{Guid.NewGuid()}\"\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n\nnamespace Grace.Server.Tests\n\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\n\n[<Parallelizable(ParallelScope.All)>]\ntype AccessControlSecurityTests() =\n\n    let createClientWithUserId (userId: string) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n        client\n\n    let grantRoleAsync (client: HttpClient) scopeKind ownerId organizationId repositoryId branchId principalId roleId =\n        task {\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.BranchId <- branchId\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- principalId\n            parameters.ScopeKind <- scopeKind\n            parameters.RoleId <- roleId\n            parameters.Source <- \"test\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            return! client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n        }\n\n    let revokeRoleAsync (client: HttpClient) scopeKind ownerId organizationId repositoryId branchId principalId roleId =\n        task {\n            let parameters = Parameters.Access.RevokeRoleParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.BranchId <- branchId\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- principalId\n            parameters.ScopeKind <- scopeKind\n            parameters.RoleId <- roleId\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            return! client.PostAsync(\"/access/revokeRole\", createJsonContent parameters)\n        }\n\n    let listRoleAssignmentsAsync (client: HttpClient) scopeKind ownerId organizationId repositoryId branchId =\n        task {\n            let parameters = Parameters.Access.ListRoleAssignmentsParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.BranchId <- branchId\n            parameters.ScopeKind <- scopeKind\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            return! client.PostAsync(\"/access/listRoleAssignments\", createJsonContent parameters)\n        }\n\n    [<Test>]\n    member _.SystemScopeRequiresSystemAdmin() =\n        task {\n            let adminUser = $\"{Guid.NewGuid()}\"\n            let nonAdminUser = $\"{Guid.NewGuid()}\"\n\n            let! grantAdmin = grantRoleAsync Client \"system\" \"\" \"\" \"\" \"\" adminUser \"SystemAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use adminClient = createClientWithUserId adminUser\n            use nonAdminClient = createClientWithUserId nonAdminUser\n\n            let! allowed = grantRoleAsync adminClient \"system\" \"\" \"\" \"\" \"\" $\"{Guid.NewGuid()}\" \"SystemAdmin\"\n            Assert.That(allowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! forbidden = grantRoleAsync nonAdminClient \"system\" \"\" \"\" \"\" \"\" $\"{Guid.NewGuid()}\" \"SystemAdmin\"\n            Assert.That(forbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.OwnerScopeBoundariesEnforced() =\n        task {\n            let ownerA = $\"{Guid.NewGuid()}\"\n            let ownerB = $\"{Guid.NewGuid()}\"\n            let adminUser = $\"{Guid.NewGuid()}\"\n\n            let! grantAdmin = grantRoleAsync Client \"owner\" ownerA \"\" \"\" \"\" adminUser \"OwnerAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use adminClient = createClientWithUserId adminUser\n\n            let! allowed = grantRoleAsync adminClient \"owner\" ownerA \"\" \"\" \"\" $\"{Guid.NewGuid()}\" \"OwnerReader\"\n            Assert.That(allowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! forbidden = grantRoleAsync adminClient \"owner\" ownerB \"\" \"\" \"\" $\"{Guid.NewGuid()}\" \"OwnerReader\"\n            Assert.That(forbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! revokeForbidden = revokeRoleAsync adminClient \"owner\" ownerB \"\" \"\" \"\" $\"{Guid.NewGuid()}\" \"OwnerReader\"\n            Assert.That(revokeForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! listAllowed = listRoleAssignmentsAsync adminClient \"owner\" ownerA \"\" \"\" \"\"\n            Assert.That(listAllowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! listForbidden = listRoleAssignmentsAsync adminClient \"owner\" ownerB \"\" \"\" \"\"\n            Assert.That(listForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.OrganizationScopeBoundariesEnforced() =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let orgA = $\"{Guid.NewGuid()}\"\n            let orgB = $\"{Guid.NewGuid()}\"\n            let adminUser = $\"{Guid.NewGuid()}\"\n\n            let! grantAdmin = grantRoleAsync Client \"org\" ownerId orgA \"\" \"\" adminUser \"OrgAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use adminClient = createClientWithUserId adminUser\n\n            let! allowed = grantRoleAsync adminClient \"org\" ownerId orgA \"\" \"\" $\"{Guid.NewGuid()}\" \"OrgReader\"\n            Assert.That(allowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! forbidden = grantRoleAsync adminClient \"org\" ownerId orgB \"\" \"\" $\"{Guid.NewGuid()}\" \"OrgReader\"\n            Assert.That(forbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! revokeForbidden = revokeRoleAsync adminClient \"org\" ownerId orgB \"\" \"\" $\"{Guid.NewGuid()}\" \"OrgReader\"\n            Assert.That(revokeForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! listAllowed = listRoleAssignmentsAsync adminClient \"org\" ownerId orgA \"\" \"\"\n            Assert.That(listAllowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! listForbidden = listRoleAssignmentsAsync adminClient \"org\" ownerId orgB \"\" \"\"\n            Assert.That(listForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.RepositoryScopeBoundariesEnforced() =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let orgId = $\"{Guid.NewGuid()}\"\n            let repoA = $\"{Guid.NewGuid()}\"\n            let repoB = $\"{Guid.NewGuid()}\"\n            let adminUser = $\"{Guid.NewGuid()}\"\n\n            let! grantAdmin = grantRoleAsync Client \"repo\" ownerId orgId repoA \"\" adminUser \"RepoAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use adminClient = createClientWithUserId adminUser\n\n            let! allowed = grantRoleAsync adminClient \"repo\" ownerId orgId repoA \"\" $\"{Guid.NewGuid()}\" \"RepoReader\"\n            Assert.That(allowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! forbidden = grantRoleAsync adminClient \"repo\" ownerId orgId repoB \"\" $\"{Guid.NewGuid()}\" \"RepoReader\"\n            Assert.That(forbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! revokeForbidden = revokeRoleAsync adminClient \"repo\" ownerId orgId repoB \"\" $\"{Guid.NewGuid()}\" \"RepoReader\"\n            Assert.That(revokeForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! listAllowed = listRoleAssignmentsAsync adminClient \"repo\" ownerId orgId repoA \"\"\n            Assert.That(listAllowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! listForbidden = listRoleAssignmentsAsync adminClient \"repo\" ownerId orgId repoB \"\"\n            Assert.That(listForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.BranchScopeBoundariesEnforced() =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let orgId = $\"{Guid.NewGuid()}\"\n            let repoId = $\"{Guid.NewGuid()}\"\n            let branchA = $\"{Guid.NewGuid()}\"\n            let branchB = $\"{Guid.NewGuid()}\"\n            let adminUser = $\"{Guid.NewGuid()}\"\n\n            let! grantAdmin = grantRoleAsync Client \"branch\" ownerId orgId repoId branchA adminUser \"BranchAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use adminClient = createClientWithUserId adminUser\n\n            let! allowed = grantRoleAsync adminClient \"branch\" ownerId orgId repoId branchA $\"{Guid.NewGuid()}\" \"BranchReader\"\n            Assert.That(allowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! forbidden = grantRoleAsync adminClient \"branch\" ownerId orgId repoId branchB $\"{Guid.NewGuid()}\" \"BranchReader\"\n            Assert.That(forbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! revokeForbidden = revokeRoleAsync adminClient \"branch\" ownerId orgId repoId branchB $\"{Guid.NewGuid()}\" \"BranchReader\"\n            Assert.That(revokeForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! listAllowed = listRoleAssignmentsAsync adminClient \"branch\" ownerId orgId repoId branchA\n            Assert.That(listAllowed.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! listForbidden = listRoleAssignmentsAsync adminClient \"branch\" ownerId orgId repoId branchB\n            Assert.That(listForbidden.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.PrivilegeEscalationPrevention() =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let orgId = $\"{Guid.NewGuid()}\"\n            let repoId = $\"{Guid.NewGuid()}\"\n\n            let repoAdminUser = $\"{Guid.NewGuid()}\"\n            let orgAdminUser = $\"{Guid.NewGuid()}\"\n            let repoWriterUser = $\"{Guid.NewGuid()}\"\n\n            let! grantRepoAdmin = grantRoleAsync Client \"repo\" ownerId orgId repoId \"\" repoAdminUser \"RepoAdmin\"\n            Assert.That(grantRepoAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantOrgAdmin = grantRoleAsync Client \"org\" ownerId orgId \"\" \"\" orgAdminUser \"OrgAdmin\"\n            Assert.That(grantOrgAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantRepoWrite = grantRoleAsync Client \"repo\" ownerId orgId repoId \"\" repoWriterUser \"RepoContributor\"\n            Assert.That(grantRepoWrite.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use repoAdminClient = createClientWithUserId repoAdminUser\n            use orgAdminClient = createClientWithUserId orgAdminUser\n            use repoWriterClient = createClientWithUserId repoWriterUser\n\n            let! repoAdminCannotGrantOrg = grantRoleAsync repoAdminClient \"org\" ownerId orgId \"\" \"\" $\"{Guid.NewGuid()}\" \"OrgAdmin\"\n\n            Assert.That(repoAdminCannotGrantOrg.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! orgAdminCannotGrantOwner = grantRoleAsync orgAdminClient \"owner\" ownerId \"\" \"\" \"\" $\"{Guid.NewGuid()}\" \"OwnerAdmin\"\n\n            Assert.That(orgAdminCannotGrantOwner.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! repoWriterCannotGrantRepoAdmin = grantRoleAsync repoWriterClient \"repo\" ownerId orgId repoId \"\" $\"{Guid.NewGuid()}\" \"RepoAdmin\"\n\n            Assert.That(repoWriterCannotGrantRepoAdmin.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.GrantRoleRejectsInvalidRole() =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let orgId = $\"{Guid.NewGuid()}\"\n            let repoId = $\"{Guid.NewGuid()}\"\n\n            let! response = grantRoleAsync Client \"repo\" ownerId orgId repoId \"\" $\"{Guid.NewGuid()}\" \"NotARole\"\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n        }\n\n    [<Test>]\n    member _.GrantRoleRejectsRoleScopeMismatch() =\n        task {\n            let ownerId = $\"{Guid.NewGuid()}\"\n            let orgId = $\"{Guid.NewGuid()}\"\n\n            let! response = grantRoleAsync Client \"org\" ownerId orgId \"\" \"\" $\"{Guid.NewGuid()}\" \"RepoAdmin\"\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n        }\n\n\nnamespace Grace.Server.Tests\n\nopen FSharp.Control\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\nopen System.Threading.Tasks\n\n[<Parallelizable(ParallelScope.All)>]\ntype AccessControl() =\n\n    let createOrganizationAsync (client: HttpClient) =\n        task {\n            let organizationId = $\"{Guid.NewGuid()}\"\n\n            let parameters = Parameters.Organization.CreateOrganizationParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.OrganizationName <- $\"AuthzOrg{Guid.NewGuid():N}\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/organization/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            return organizationId\n        }\n\n    let createRepositoryAsync (client: HttpClient) (organizationId: string) =\n        task {\n            let repositoryId = $\"{Guid.NewGuid()}\"\n\n            let parameters = Parameters.Repository.CreateRepositoryParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.RepositoryName <- $\"AuthzRepo{Guid.NewGuid():N}\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/repository/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            return repositoryId\n        }\n\n    let grantRoleAsync (client: HttpClient) ownerId organizationId repositoryId scopeKind roleId principalId =\n        task {\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- principalId\n            parameters.ScopeKind <- scopeKind\n            parameters.RoleId <- roleId\n            parameters.Source <- \"test\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n        }\n\n    let upsertPathPermissionAsync (client: HttpClient) ownerId organizationId repositoryId path claimPermissions =\n        task {\n            let parameters = Parameters.Access.UpsertPathPermissionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.Path <- path\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            for (claim, permission) in claimPermissions do\n                let claimPermission = Parameters.Access.ClaimPermissionParameters()\n                claimPermission.Claim <- claim\n                claimPermission.DirectoryPermission <- permission\n                parameters.ClaimPermissions.Add(claimPermission)\n\n            let! response = client.PostAsync(\"/access/upsertPathPermission\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n        }\n\n    let checkPermissionAsync (client: HttpClient) ownerId organizationId repositoryId resourceKind operation path =\n        task {\n            let parameters = Parameters.Access.CheckPermissionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.ResourceKind <- resourceKind\n            parameters.Operation <- operation\n            parameters.Path <- path\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/access/checkPermission\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<PermissionCheckResult>> response\n            return returnValue.ReturnValue\n        }\n\n    let createClientWithClaims (claims: string list) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", testUserId)\n\n        if not (List.isEmpty claims) then\n            client.DefaultRequestHeaders.Add(\"x-grace-claims\", String.Join(\";\", claims))\n\n        client\n\n    let createClientWithUserId (userId: string) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n        client\n\n    [<Test>]\n    member _.ProtectedEndpointsRequireAuthentication() =\n        task {\n            use unauthenticatedClient = new HttpClient()\n            unauthenticatedClient.BaseAddress <- Client.BaseAddress\n\n            let parameters = Parameters.Repository.SetRepositoryDescriptionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[0]\n            parameters.Description <- \"Authz test description\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = unauthenticatedClient.PostAsync(\"/repository/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n        }\n\n    [<Test>]\n    member _.RoleInheritanceResolvesPermissionsAcrossOrganizations() =\n        task {\n            let! orgA = createOrganizationAsync Client\n            let! orgB = createOrganizationAsync Client\n            let! repoA = createRepositoryAsync Client orgA\n            let! repoB = createRepositoryAsync Client orgB\n\n            let nonAdminUserId = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUserId\n\n            do! grantRoleAsync Client ownerId orgA \"\" \"org\" \"OrgAdmin\" nonAdminUserId\n            do! grantRoleAsync Client ownerId orgB \"\" \"org\" \"OrgReader\" nonAdminUserId\n\n            let! repoAWrite = checkPermissionAsync nonAdminClient ownerId orgA repoA \"repo\" \"RepoWrite\" \"\"\n            let! repoBWrite = checkPermissionAsync nonAdminClient ownerId orgB repoB \"repo\" \"RepoWrite\" \"\"\n            let! repoBRead = checkPermissionAsync nonAdminClient ownerId orgB repoB \"repo\" \"RepoRead\" \"\"\n\n            match repoAWrite with\n            | Allowed _ -> ()\n            | Denied reason -> Assert.Fail($\"Expected repoA write to be allowed. {reason}\")\n\n            match repoBWrite with\n            | Denied _ -> ()\n            | Allowed reason -> Assert.Fail($\"Expected repoB write to be denied. {reason}\")\n\n            match repoBRead with\n            | Allowed _ -> ()\n            | Denied reason -> Assert.Fail($\"Expected repoB read to be allowed. {reason}\")\n        }\n\n    [<Test>]\n    member _.PathPermissionDenyOverridesAllowInCheckPermission() =\n        task {\n            let! organizationId = createOrganizationAsync Client\n            let! repositoryId = createRepositoryAsync Client organizationId\n\n            do! grantRoleAsync Client ownerId organizationId \"\" \"org\" \"OrgAdmin\" testUserId\n\n            do!\n                upsertPathPermissionAsync\n                    Client\n                    ownerId\n                    organizationId\n                    repositoryId\n                    \"/images\"\n                    [\n                        (\"engineering\", \"NoAccess\")\n                        (\"writers\", \"Modify\")\n                    ]\n\n            use claimsClient =\n                createClientWithClaims [ \"engineering\"\n                                         \"writers\" ]\n\n            let! permission = checkPermissionAsync claimsClient ownerId organizationId repositoryId \"path\" \"PathWrite\" \"/images\"\n\n            match permission with\n            | Denied _ -> ()\n            | Allowed reason -> Assert.Fail($\"Expected path write to be denied. {reason}\")\n\n            do! upsertPathPermissionAsync Client ownerId organizationId repositoryId \"/images\" [ (\"engineering\", \"Modify\") ]\n\n            let! allowedPermission = checkPermissionAsync claimsClient ownerId organizationId repositoryId \"path\" \"PathWrite\" \"/images\"\n\n            match allowedPermission with\n            | Allowed _ -> ()\n            | Denied reason -> Assert.Fail($\"Expected path write to be allowed. {reason}\")\n        }\n\n    [<Test>]\n    member _.RepositoryEndpointRequiresRoleAssignment() =\n        task {\n            let! orgId = createOrganizationAsync Client\n            let! repoId = createRepositoryAsync Client orgId\n\n            let parameters = Parameters.Repository.SetRepositoryDescriptionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- orgId\n            parameters.RepositoryId <- repoId\n            parameters.Description <- \"Repo description update\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let nonAdminUserId = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUserId\n\n            let! deniedResponse = nonAdminClient.PostAsync(\"/repository/setDescription\", createJsonContent parameters)\n            Assert.That(deniedResponse.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            do! grantRoleAsync Client ownerId orgId \"\" \"org\" \"OrgAdmin\" nonAdminUserId\n\n            let! allowedResponse = nonAdminClient.PostAsync(\"/repository/setDescription\", createJsonContent parameters)\n            Assert.That(allowedResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.StorageUploadMetadataRespectsPathPermissions() =\n        task {\n            let! orgId = createOrganizationAsync Client\n            let! repoId = createRepositoryAsync Client orgId\n\n            do! grantRoleAsync Client ownerId orgId \"\" \"org\" \"OrgAdmin\" testUserId\n\n            do!\n                upsertPathPermissionAsync\n                    Client\n                    ownerId\n                    orgId\n                    repoId\n                    \"images/foo.png\"\n                    [\n                        (\"engineering\", \"NoAccess\")\n                        (\"writers\", \"Modify\")\n                    ]\n\n            use claimsClient =\n                createClientWithClaims [ \"engineering\"\n                                         \"writers\" ]\n\n            let fileVersion = FileVersion.Create \"images/foo.png\" \"hash\" \"\" false 1L\n\n            let uploadParameters = Parameters.Storage.GetUploadMetadataForFilesParameters()\n            uploadParameters.OwnerId <- ownerId\n            uploadParameters.OrganizationId <- orgId\n            uploadParameters.RepositoryId <- repoId\n            uploadParameters.FileVersions <- [| fileVersion |]\n            uploadParameters.CorrelationId <- generateCorrelationId ()\n\n            let! deniedResponse = claimsClient.PostAsync(\"/storage/getUploadMetadataForFiles\", createJsonContent uploadParameters)\n            Assert.That(deniedResponse.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            do! upsertPathPermissionAsync Client ownerId orgId repoId \"images/foo.png\" [ (\"engineering\", \"Modify\") ]\n\n            let! allowedResponse = claimsClient.PostAsync(\"/storage/getUploadMetadataForFiles\", createJsonContent uploadParameters)\n            Assert.That(allowedResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.AccessGrantRoleRequiresAdminScope() =\n        task {\n            let nonAdminUserId = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUserId\n\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.ScopeKind <- \"system\"\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- nonAdminUserId\n            parameters.RoleId <- \"SystemAdmin\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = nonAdminClient.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.AccessGrantRoleValidatesRoleId() =\n        task {\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.ScopeKind <- \"system\"\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- testUserId\n            parameters.RoleId <- \"NotARealRole\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n        }\n\n    [<Test>]\n    member _.AccessGrantRoleValidatesScopeApplicability() =\n        task {\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.OwnerId <- ownerId\n            parameters.ScopeKind <- \"owner\"\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- testUserId\n            parameters.RoleId <- \"RepoAdmin\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n        }\n\n    [<Test>]\n    member _.BootstrapSeedsSystemAdmin() =\n        task {\n            let parameters = Parameters.Access.ListRoleAssignmentsParameters()\n            parameters.ScopeKind <- \"system\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/access/listRoleAssignments\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n\n            let! returnValue = deserializeContent<GraceReturnValue<RoleAssignment list>> response\n\n            let seeded =\n                returnValue.ReturnValue\n                |> List.exists (fun assignment ->\n                    assignment.RoleId.Equals(\"SystemAdmin\", StringComparison.OrdinalIgnoreCase)\n                    && assignment.Principal.PrincipalId = testUserId\n                    && assignment.Source = \"bootstrap\")\n\n            Assert.That(seeded, Is.True)\n        }\n\n    [<Test>]\n    member _.AccessListRoleAssignmentsRequiresAdminScope() =\n        task {\n            let nonAdminUserId = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUserId\n\n            let parameters = Parameters.Access.ListRoleAssignmentsParameters()\n            parameters.OwnerId <- ownerId\n            parameters.ScopeKind <- \"owner\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = nonAdminClient.PostAsync(\"/access/listRoleAssignments\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.AccessCheckPermissionRestrictsOtherPrincipals() =\n        task {\n            let nonAdminUserId = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUserId\n\n            let parameters = Parameters.Access.CheckPermissionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[0]\n            parameters.ResourceKind <- \"repo\"\n            parameters.Operation <- \"RepoRead\"\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- \"other-user\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = nonAdminClient.PostAsync(\"/access/checkPermission\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n        }\n\n    [<Test>]\n    member _.AccessCheckPermissionAllowsSelfPrincipal() =\n        task {\n            let nonAdminUserId = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUserId\n\n            let parameters = Parameters.Access.CheckPermissionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[0]\n            parameters.ResourceKind <- \"repo\"\n            parameters.Operation <- \"RepoRead\"\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- nonAdminUserId\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = nonAdminClient.PostAsync(\"/access/checkPermission\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n\nnamespace Grace.Server.Tests\n\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Branch\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\n\n[<Parallelizable(ParallelScope.All)>]\ntype EndpointAuthorizationTests() =\n\n    let createClientWithUserId (userId: string) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n        client\n\n    let createUnauthenticatedClient () =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client\n\n    let grantRoleAsync (client: HttpClient) scopeKind ownerId organizationId repositoryId branchId principalId roleId =\n        task {\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.BranchId <- branchId\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- principalId\n            parameters.ScopeKind <- scopeKind\n            parameters.RoleId <- roleId\n            parameters.Source <- \"test\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            return! client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n        }\n\n    let getDefaultBranchAsync (repositoryId: string) =\n        task {\n            let repositoryIndex = repositoryIds |> Array.findIndex (fun candidate -> candidate = repositoryId)\n            let branchId = repositoryDefaultBranchIds[repositoryIndex]\n            let parameters = Parameters.Branch.GetBranchParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.BranchId <- branchId\n            let timeoutAt = DateTime.UtcNow.AddSeconds(10.0)\n            let mutable branch = None\n            let mutable lastStatusCode = HttpStatusCode.OK\n            let mutable lastResponseBody = String.Empty\n\n            while branch.IsNone && DateTime.UtcNow < timeoutAt do\n                parameters.CorrelationId <- generateCorrelationId ()\n\n                let! response = Client.PostAsync(\"/branch/get\", createJsonContent parameters)\n                lastStatusCode <- response.StatusCode\n\n                let! responseBody = response.Content.ReadAsStringAsync()\n                lastResponseBody <- responseBody\n\n                if response.IsSuccessStatusCode then\n                    let returnValue = deserialize<GraceReturnValue<BranchDto>> responseBody\n                    let branchDto = returnValue.ReturnValue\n\n                    if branchDto.BranchId <> Guid.Empty && branchDto.LatestPromotion.ReferenceId <> Guid.Empty then\n                        branch <- Some branchDto\n\n                if branch.IsNone then\n                    do! System.Threading.Tasks.Task.Delay(TimeSpan.FromMilliseconds(250.0))\n\n            match branch with\n            | Some branch -> return branch\n            | None ->\n                Assert.Fail(\n                    $\"Timed out waiting for repository {repositoryId} to expose initial branch {branchId} through /branch/get. Last status: {lastStatusCode}; body: {lastResponseBody}\"\n                )\n\n                return Unchecked.defaultof<BranchDto>\n        }\n\n    let createOwnerGetParameters () =\n        let parameters = Parameters.Owner.GetOwnerParameters()\n        parameters.OwnerId <- ownerId\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createOwnerSetNameParameters () =\n        let parameters = Parameters.Owner.SetOwnerNameParameters()\n        parameters.OwnerId <- ownerId\n        parameters.NewName <- $\"Owner{Guid.NewGuid():N}\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createOrganizationGetParameters () =\n        let parameters = Parameters.Organization.GetOrganizationParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createOrganizationSetNameParameters () =\n        let parameters = Parameters.Organization.SetOrganizationNameParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.NewName <- $\"Org{Guid.NewGuid():N}\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createRepositoryGetParameters (repositoryId: string) =\n        let parameters = Parameters.Repository.GetRepositoryParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- repositoryId\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createRepositorySetVisibilityParameters (repositoryId: string) =\n        let parameters = Parameters.Repository.SetRepositoryVisibilityParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- repositoryId\n        parameters.Visibility <- \"Public\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createWorkItemParameters (repositoryId: string) =\n        let parameters = Parameters.WorkItem.CreateWorkItemParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- repositoryId\n        parameters.WorkItemId <- $\"{Guid.NewGuid()}\"\n        parameters.Title <- $\"Work{Guid.NewGuid():N}\"\n        parameters.Description <- \"Test work item\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createBranchGetParameters (branch: BranchDto) =\n        let parameters = Parameters.Branch.GetBranchParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- $\"{branch.RepositoryId}\"\n        parameters.BranchId <- $\"{branch.BranchId}\"\n        parameters.BranchName <- $\"{branch.BranchName}\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createBranchCommitParameters (branch: BranchDto) =\n        let parameters = Parameters.Branch.CreateReferenceParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- $\"{branch.RepositoryId}\"\n        parameters.BranchId <- $\"{branch.BranchId}\"\n        parameters.BranchName <- $\"{branch.BranchName}\"\n        parameters.DirectoryVersionId <- branch.LatestPromotion.DirectoryId\n        parameters.Sha256Hash <- $\"{branch.LatestPromotion.Sha256Hash}\"\n        parameters.Message <- \"Commit from test\"\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createBranchEnableCommitParameters (branch: BranchDto) =\n        let parameters = Parameters.Branch.EnableFeatureParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- $\"{branch.RepositoryId}\"\n        parameters.BranchId <- $\"{branch.BranchId}\"\n        parameters.BranchName <- $\"{branch.BranchName}\"\n        parameters.Enabled <- true\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createDownloadParameters (repositoryId: string) =\n        let parameters = Parameters.Storage.GetDownloadUriParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- repositoryId\n        parameters.FileVersion <- FileVersion.Create \"images/test.txt\" \"hash\" \"\" false 1L\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    let createUploadParameters (repositoryId: string) =\n        let parameters = Parameters.Storage.GetUploadUriParameters()\n        parameters.OwnerId <- ownerId\n        parameters.OrganizationId <- organizationId\n        parameters.RepositoryId <- repositoryId\n\n        parameters.FileVersions <-\n            [|\n                FileVersion.Create \"images/test.txt\" \"hash\" \"\" false 1L\n            |]\n\n        parameters.CorrelationId <- generateCorrelationId ()\n        parameters\n\n    [<Test>]\n    member _.AllowAnonymousEndpointsReturnSuccess() =\n        task {\n            use client = createUnauthenticatedClient ()\n\n            let! rootResponse = client.GetAsync(\"/\")\n            Assert.That(rootResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! healthResponse = client.GetAsync(\"/healthz\")\n            Assert.That(healthResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! configResponse = client.GetAsync(\"/auth/oidc/config\")\n            Assert.That(configResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! loginResponse = client.GetAsync(\"/auth/login\")\n            Assert.That(loginResponse.StatusCode, Is.AnyOf(HttpStatusCode.OK, HttpStatusCode.Redirect))\n\n            let! providerLoginResponse = client.GetAsync(\"/auth/login/test\")\n            Assert.That(providerLoginResponse.StatusCode, Is.AnyOf(HttpStatusCode.OK, HttpStatusCode.Redirect, HttpStatusCode.NotFound))\n        }\n\n    [<Test>]\n    member _.MetricsEndpointRequiresSystemAdmin() =\n        task {\n            use unauthClient = createUnauthenticatedClient ()\n            let! unauthResponse = unauthClient.GetAsync(\"/metrics\")\n            Assert.That(unauthResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let nonAdminUser = $\"{Guid.NewGuid()}\"\n            use nonAdminClient = createClientWithUserId nonAdminUser\n            let! nonAdminResponse = nonAdminClient.GetAsync(\"/metrics\")\n            Assert.That(nonAdminResponse.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! adminResponse = Client.GetAsync(\"/metrics\")\n            Assert.That(adminResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.OwnerEndpointsRequireAuthorization() =\n        task {\n            let ownerReader = $\"{Guid.NewGuid()}\"\n            let ownerAdmin = $\"{Guid.NewGuid()}\"\n\n            let! grantReader = grantRoleAsync Client \"owner\" ownerId \"\" \"\" \"\" ownerReader \"OwnerReader\"\n            Assert.That(grantReader.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantAdmin = grantRoleAsync Client \"owner\" ownerId \"\" \"\" \"\" ownerAdmin \"OwnerAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use unauthClient = createUnauthenticatedClient ()\n            use readerClient = createClientWithUserId ownerReader\n            use adminClient = createClientWithUserId ownerAdmin\n            use unprivilegedClient = createClientWithUserId $\"{Guid.NewGuid()}\"\n\n            let! unauthGet = unauthClient.PostAsync(\"/owner/get\", createJsonContent (createOwnerGetParameters ()))\n            Assert.That(unauthGet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedGet = unprivilegedClient.PostAsync(\"/owner/get\", createJsonContent (createOwnerGetParameters ()))\n            Assert.That(deniedGet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedGet = readerClient.PostAsync(\"/owner/get\", createJsonContent (createOwnerGetParameters ()))\n            Assert.That(allowedGet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! unauthSet = unauthClient.PostAsync(\"/owner/setName\", createJsonContent (createOwnerSetNameParameters ()))\n\n            Assert.That(unauthSet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedSet = readerClient.PostAsync(\"/owner/setName\", createJsonContent (createOwnerSetNameParameters ()))\n\n            Assert.That(deniedSet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedSet = adminClient.PostAsync(\"/owner/setName\", createJsonContent (createOwnerSetNameParameters ()))\n\n            Assert.That(allowedSet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.OrganizationEndpointsRequireAuthorization() =\n        task {\n            let orgReader = $\"{Guid.NewGuid()}\"\n            let orgAdmin = $\"{Guid.NewGuid()}\"\n\n            let! grantReader = grantRoleAsync Client \"org\" ownerId organizationId \"\" \"\" orgReader \"OrgReader\"\n            Assert.That(grantReader.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantAdmin = grantRoleAsync Client \"org\" ownerId organizationId \"\" \"\" orgAdmin \"OrgAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use unauthClient = createUnauthenticatedClient ()\n            use readerClient = createClientWithUserId orgReader\n            use adminClient = createClientWithUserId orgAdmin\n            use unprivilegedClient = createClientWithUserId $\"{Guid.NewGuid()}\"\n\n            let! unauthGet = unauthClient.PostAsync(\"/organization/get\", createJsonContent (createOrganizationGetParameters ()))\n            Assert.That(unauthGet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedGet = unprivilegedClient.PostAsync(\"/organization/get\", createJsonContent (createOrganizationGetParameters ()))\n            Assert.That(deniedGet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedGet = readerClient.PostAsync(\"/organization/get\", createJsonContent (createOrganizationGetParameters ()))\n            Assert.That(allowedGet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! unauthSet = unauthClient.PostAsync(\"/organization/setName\", createJsonContent (createOrganizationSetNameParameters ()))\n\n            Assert.That(unauthSet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedSet = readerClient.PostAsync(\"/organization/setName\", createJsonContent (createOrganizationSetNameParameters ()))\n\n            Assert.That(deniedSet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedSet = adminClient.PostAsync(\"/organization/setName\", createJsonContent (createOrganizationSetNameParameters ()))\n\n            Assert.That(allowedSet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.RepositoryEndpointsRequireAuthorization() =\n        task {\n            let repositoryId = repositoryIds[0]\n            let repoReader = $\"{Guid.NewGuid()}\"\n            let repoAdmin = $\"{Guid.NewGuid()}\"\n\n            let! grantReader = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" repoReader \"RepoReader\"\n            Assert.That(grantReader.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantAdmin = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" repoAdmin \"RepoAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use unauthClient = createUnauthenticatedClient ()\n            use readerClient = createClientWithUserId repoReader\n            use adminClient = createClientWithUserId repoAdmin\n            use unprivilegedClient = createClientWithUserId $\"{Guid.NewGuid()}\"\n\n            let! unauthGet = unauthClient.PostAsync(\"/repository/get\", createJsonContent (createRepositoryGetParameters repositoryId))\n            Assert.That(unauthGet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedGet = unprivilegedClient.PostAsync(\"/repository/get\", createJsonContent (createRepositoryGetParameters repositoryId))\n            Assert.That(deniedGet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedGet = readerClient.PostAsync(\"/repository/get\", createJsonContent (createRepositoryGetParameters repositoryId))\n            Assert.That(allowedGet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! unauthSet = unauthClient.PostAsync(\"/repository/setVisibility\", createJsonContent (createRepositorySetVisibilityParameters repositoryId))\n\n            Assert.That(unauthSet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedSet = readerClient.PostAsync(\"/repository/setVisibility\", createJsonContent (createRepositorySetVisibilityParameters repositoryId))\n\n            Assert.That(deniedSet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedSet = adminClient.PostAsync(\"/repository/setVisibility\", createJsonContent (createRepositorySetVisibilityParameters repositoryId))\n\n            Assert.That(allowedSet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.WorkCreateRequiresRepoWrite() =\n        task {\n            let repositoryId = repositoryIds[0]\n            let repoReader = $\"{Guid.NewGuid()}\"\n            let repoWriter = $\"{Guid.NewGuid()}\"\n\n            let! grantReader = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" repoReader \"RepoReader\"\n            Assert.That(grantReader.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantWriter = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" repoWriter \"RepoContributor\"\n            Assert.That(grantWriter.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use unauthClient = createUnauthenticatedClient ()\n            use readerClient = createClientWithUserId repoReader\n            use writerClient = createClientWithUserId repoWriter\n\n            let! unauthResponse = unauthClient.PostAsync(\"/work/create\", createJsonContent (createWorkItemParameters repositoryId))\n\n            Assert.That(unauthResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedResponse = readerClient.PostAsync(\"/work/create\", createJsonContent (createWorkItemParameters repositoryId))\n\n            Assert.That(deniedResponse.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedResponse = writerClient.PostAsync(\"/work/create\", createJsonContent (createWorkItemParameters repositoryId))\n\n            Assert.That(allowedResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.BranchEndpointsRequireAuthorization() =\n        task {\n            let repositoryId = repositoryIds[0]\n            let! branch = getDefaultBranchAsync repositoryId\n\n            let branchReader = $\"{Guid.NewGuid()}\"\n            let branchWriter = $\"{Guid.NewGuid()}\"\n            let branchAdmin = $\"{Guid.NewGuid()}\"\n\n            let! grantReader = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" branchReader \"RepoReader\"\n            Assert.That(grantReader.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantWriter = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" branchWriter \"RepoContributor\"\n            Assert.That(grantWriter.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantAdmin = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" branchAdmin \"RepoAdmin\"\n            Assert.That(grantAdmin.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use unauthClient = createUnauthenticatedClient ()\n            use readerClient = createClientWithUserId branchReader\n            use writerClient = createClientWithUserId branchWriter\n            use adminClient = createClientWithUserId branchAdmin\n            use unprivilegedClient = createClientWithUserId $\"{Guid.NewGuid()}\"\n\n            let! unauthGet = unauthClient.PostAsync(\"/branch/get\", createJsonContent (createBranchGetParameters branch))\n            Assert.That(unauthGet.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedGet = unprivilegedClient.PostAsync(\"/branch/get\", createJsonContent (createBranchGetParameters branch))\n            Assert.That(deniedGet.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedGet = readerClient.PostAsync(\"/branch/get\", createJsonContent (createBranchGetParameters branch))\n            Assert.That(allowedGet.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! enableCommitResponse = adminClient.PostAsync(\"/branch/enableCommit\", createJsonContent (createBranchEnableCommitParameters branch))\n\n            Assert.That(enableCommitResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! deniedEnable = writerClient.PostAsync(\"/branch/enableCommit\", createJsonContent (createBranchEnableCommitParameters branch))\n\n            Assert.That(deniedEnable.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! unauthCommit = unauthClient.PostAsync(\"/branch/commit\", createJsonContent (createBranchCommitParameters branch))\n\n            Assert.That(unauthCommit.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedCommit = unprivilegedClient.PostAsync(\"/branch/commit\", createJsonContent (createBranchCommitParameters branch))\n\n            Assert.That(deniedCommit.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedCommit = writerClient.PostAsync(\"/branch/commit\", createJsonContent (createBranchCommitParameters branch))\n\n            Assert.That(allowedCommit.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n    [<Test>]\n    member _.StorageEndpointsRequirePathAuthorization() =\n        task {\n            let repositoryId = repositoryIds[0]\n            let pathReader = $\"{Guid.NewGuid()}\"\n            let pathWriter = $\"{Guid.NewGuid()}\"\n\n            let! grantReader = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" pathReader \"RepoReader\"\n            Assert.That(grantReader.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! grantWriter = grantRoleAsync Client \"repo\" ownerId organizationId repositoryId \"\" pathWriter \"RepoContributor\"\n            Assert.That(grantWriter.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            use unauthClient = createUnauthenticatedClient ()\n            use readerClient = createClientWithUserId pathReader\n            use writerClient = createClientWithUserId pathWriter\n            use unprivilegedClient = createClientWithUserId $\"{Guid.NewGuid()}\"\n\n            let! unauthDownload = unauthClient.PostAsync(\"/storage/getDownloadUri\", createJsonContent (createDownloadParameters repositoryId))\n\n            Assert.That(unauthDownload.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedDownload = unprivilegedClient.PostAsync(\"/storage/getDownloadUri\", createJsonContent (createDownloadParameters repositoryId))\n\n            Assert.That(deniedDownload.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedDownload = readerClient.PostAsync(\"/storage/getDownloadUri\", createJsonContent (createDownloadParameters repositoryId))\n\n            Assert.That(allowedDownload.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! unauthUpload = unauthClient.PostAsync(\"/storage/getUploadUri\", createJsonContent (createUploadParameters repositoryId))\n\n            Assert.That(unauthUpload.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n\n            let! deniedUpload = unprivilegedClient.PostAsync(\"/storage/getUploadUri\", createJsonContent (createUploadParameters repositoryId))\n\n            Assert.That(deniedUpload.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden))\n\n            let! allowedUpload = writerClient.PostAsync(\"/storage/getUploadUri\", createJsonContent (createUploadParameters repositoryId))\n\n            Assert.That(allowedUpload.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/AspireTestHost.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Aspire.Hosting\nopen Aspire.Hosting.ApplicationModel\nopen Aspire.Hosting.Testing\nopen Aspire.Hosting.Azure\nopen Azure.Messaging.ServiceBus\nopen Grace.Shared\nopen Microsoft.Azure.Cosmos\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.Extensions.Logging\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.Net.Http\nopen System.Net.Security\nopen System.Text\nopen System.Text.Json\nopen System.Threading\nopen System.Threading.Tasks\nopen Grace.Types\nopen Grace.Shared.Utilities\n\ntype TestHostState =\n    {\n        App: DistributedApplication\n        Client: HttpClient\n        GraceServerBaseAddress: string\n        ServiceBusConnectionString: string\n        ServiceBusTopic: string\n        ServiceBusServerSubscription: string\n        ServiceBusTestSubscription: string\n    }\n\nmodule AspireTestHost =\n    do AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> Environment.ExitCode <- 0)\n\n    let private graceServerResourceName = \"grace-server\"\n    let private azuriteResourceName = \"azurite\"\n    let private sharedStateLock = new SemaphoreSlim(1, 1)\n    let mutable private sharedState: TestHostState option = None\n    let mutable private sharedBootstrapUserId: string option = None\n    let private getServiceBusSqlResourceName () =\n        match Environment.GetEnvironmentVariable(\"GRACE_TEST_RUN_ID\") with\n        | value when not (String.IsNullOrWhiteSpace value) -> $\"servicebus-sql-{value}\"\n        | _ -> \"servicebus-sql\"\n\n    let private getServiceBusEmulatorResourceName () =\n        match Environment.GetEnvironmentVariable(\"GRACE_TEST_RUN_ID\") with\n        | value when not (String.IsNullOrWhiteSpace value) -> $\"servicebus-emulator-{value}\"\n        | _ -> \"servicebus-emulator\"\n    let private isCi =\n        match Environment.GetEnvironmentVariable(\"GITHUB_ACTIONS\"), Environment.GetEnvironmentVariable(\"CI\") with\n        | value, _ when not (String.IsNullOrWhiteSpace value) -> true\n        | _, value when not (String.IsNullOrWhiteSpace value) -> true\n        | _ -> false\n\n    let private getTimeout (local: TimeSpan) (ci: TimeSpan) = if isCi then ci else local\n\n    let private defaultWaitTimeout = getTimeout (TimeSpan.FromMinutes(5.0)) (TimeSpan.FromMinutes(3.0))\n\n    let private ensureBootstrapCompatible (bootstrapUserId: string) =\n        match sharedBootstrapUserId with\n        | None -> ()\n        | Some existing when String.IsNullOrWhiteSpace bootstrapUserId ->\n            if not (String.IsNullOrWhiteSpace existing) then\n                Console.WriteLine(\n                    $\"Aspire test host already started with bootstrap user '{existing}'. Ignoring empty bootstrap request.\"\n                )\n        | Some existing when existing.Equals(bootstrapUserId, StringComparison.OrdinalIgnoreCase) -> ()\n        | Some existing ->\n            raise (\n                InvalidOperationException(\n                    $\"Aspire test host already started with bootstrap user '{existing}'. Requested '{bootstrapUserId}' cannot be applied without restarting the host.\"\n                )\n            )\n\n    let private getResource (app: DistributedApplication) (resourceName: string) =\n        let model = app.Services.GetRequiredService<DistributedApplicationModel>()\n\n        model.Resources\n        |> Seq.tryFind (fun resource -> resource.Name = resourceName)\n        |> Option.defaultWith (fun () -> failwith $\"Resource '{resourceName}' not found in distributed application model.\")\n\n    let private tryFindResourceName<'T when 'T :> IResource> (app: DistributedApplication) =\n        let model = app.Services.GetRequiredService<DistributedApplicationModel>()\n\n        model.Resources\n        |> Seq.tryFind (fun resource -> resource :? 'T)\n        |> Option.map (fun resource -> resource.Name)\n\n    let private tryFindResourceByName (app: DistributedApplication) (resourceName: string) =\n        let model = app.Services.GetRequiredService<DistributedApplicationModel>()\n\n        model.Resources\n        |> Seq.tryFind (fun resource -> resource.Name = resourceName)\n\n    let private getEndpointName (app: DistributedApplication) (resourceName: string) =\n        let resource = getResource app resourceName\n        let resourceWithEndpoints = resource :?> IResourceWithEndpoints\n        let endpoints = resourceWithEndpoints.GetEndpoints() |> Seq.toList\n\n        let byScheme scheme =\n            endpoints\n            |> List.tryFind (fun endpoint -> endpoint.EndpointAnnotation.UriScheme.Equals(scheme, StringComparison.OrdinalIgnoreCase))\n\n        match byScheme \"http\"\n              |> Option.orElseWith (fun () -> byScheme \"https\")\n            with\n        | Some endpoint -> endpoint.EndpointAnnotation.Name\n        | None ->\n            match endpoints with\n            | first :: _ -> first.EndpointAnnotation.Name\n            | [] -> failwith $\"No endpoints found for resource '{resourceName}'.\"\n\n    let private getEnvironmentVariablesAsync (app: DistributedApplication) (resourceName: string) =\n        task {\n            let resource = getResource app resourceName\n            let loggerFactory = app.Services.GetRequiredService<ILoggerFactory>()\n            let logger = loggerFactory.CreateLogger(\"AspireTestHost.ExecutionConfiguration\")\n            let executionContext = DistributedApplicationExecutionContext(DistributedApplicationOperation.Run)\n\n            let! configuration =\n                ExecutionConfigurationBuilder\n                    .Create(resource)\n                    .WithEnvironmentVariablesConfig()\n                    .BuildAsync(executionContext, logger, CancellationToken.None)\n\n            if not (isNull configuration.Exception) then raise configuration.Exception\n\n            let env = configuration.EnvironmentVariables\n\n            return\n                env\n                |> Seq.map (fun kvp -> kvp.Key, string kvp.Value)\n                |> Map.ofSeq\n        }\n\n    let private describeResourceState (notificationService: ResourceNotificationService) (resourceName: string) =\n        let mutable resourceEvent = Unchecked.defaultof<ResourceEvent>\n\n        if notificationService.TryGetCurrentState(resourceName, &resourceEvent) then\n            let snapshot = resourceEvent.Snapshot\n\n            let healthReports =\n                snapshot.HealthReports\n                |> Seq.map (fun report ->\n                    let status = if report.Status.HasValue then report.Status.Value.ToString() else \"Unknown\"\n\n                    let description = if String.IsNullOrWhiteSpace report.Description then \"\" else report.Description\n\n                    let exceptionText =\n                        if String.IsNullOrWhiteSpace report.ExceptionText then\n                            \"\"\n                        else\n                            report.ExceptionText\n\n                    $\"{report.Name}={status}: {description} {exceptionText}\"\n                        .Trim())\n                |> String.concat \"; \"\n\n            $\"State={snapshot.State}; Health={snapshot.HealthStatus}; ExitCode={snapshot.ExitCode}; HealthReports=[{healthReports}]\"\n        else\n            \"State unavailable.\"\n\n    let private getResourceLogsAsync (app: DistributedApplication) (resourceName: string) =\n        task {\n            let loggerService = app.Services.GetRequiredService<ResourceLoggerService>()\n            let lines = ResizeArray<string>()\n            use cts = new CancellationTokenSource(TimeSpan.FromSeconds(10.0))\n            let logs = loggerService.GetAllAsync(resourceName)\n            let enumerator = logs.GetAsyncEnumerator(cts.Token)\n\n            try\n                let mutable keepGoing = true\n\n                while keepGoing do\n                    let! hasNext = enumerator.MoveNextAsync().AsTask()\n\n                    if hasNext then\n                        let batch = enumerator.Current\n\n                        for line in batch do\n                            lines.Add(line.Content)\n                    else\n                        keepGoing <- false\n            finally\n                enumerator\n                    .DisposeAsync()\n                    .AsTask()\n                    .GetAwaiter()\n                    .GetResult()\n\n            return lines |> Seq.toList\n        }\n\n    type private ProcessResult =\n        {\n            ExitCode: int option\n            StdOut: string\n            StdErr: string\n            TimedOut: bool\n            Error: string option\n        }\n\n    let private runProcessAsync (fileName: string) (arguments: string) (timeout: TimeSpan) =\n        task {\n            try\n                let startInfo = ProcessStartInfo(fileName, arguments)\n                startInfo.RedirectStandardOutput <- true\n                startInfo.RedirectStandardError <- true\n                startInfo.UseShellExecute <- false\n                startInfo.CreateNoWindow <- true\n\n                use proc = new Process()\n                proc.StartInfo <- startInfo\n\n                if not (proc.Start()) then\n                    return { ExitCode = None; StdOut = \"\"; StdErr = \"\"; TimedOut = false; Error = Some \"Failed to start process.\" }\n                else\n                    let waitTask = proc.WaitForExitAsync()\n                    let! completed = Task.WhenAny(waitTask, Task.Delay(timeout))\n\n                    if completed <> waitTask then\n                        try\n                            proc.Kill(true)\n                        with\n                        | _ -> ()\n\n                        return { ExitCode = None; StdOut = \"\"; StdErr = \"\"; TimedOut = true; Error = None }\n                    else\n                        let! stdOut = proc.StandardOutput.ReadToEndAsync()\n                        let! stdErr = proc.StandardError.ReadToEndAsync()\n\n                        return\n                            {\n                                ExitCode = Some proc.ExitCode\n                                StdOut = stdOut\n                                StdErr = stdErr\n                                TimedOut = false\n                                Error = None\n                            }\n            with\n            | ex ->\n                return { ExitCode = None; StdOut = \"\"; StdErr = \"\"; TimedOut = false; Error = Some ex.Message }\n        }\n\n    let private shouldCleanupDocker () =\n        match Environment.GetEnvironmentVariable(\"GRACE_TEST_CLEANUP\") with\n        | null -> false\n        | value when value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                     || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n                     || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase) -> true\n        | _ -> false\n\n    let private shouldSkipServiceBus () =\n        match Environment.GetEnvironmentVariable(\"GRACE_TEST_SKIP_SERVICEBUS\") with\n        | null -> false\n        | value when value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                     || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n                     || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase) -> true\n        | _ -> false\n\n    let private cleanupDockerContainersAsync () =\n        task {\n            if shouldCleanupDocker () then\n                let containerPrefixes =\n                    [ \"servicebus-sql\"\n                      \"servicebus-emulator\"\n                      \"cosmosdb-emulator\"\n                      \"azurite\"\n                      \"redis\" ]\n\n                let! listResult = runProcessAsync \"docker\" \"ps -a --format \\\"{{.Names}}\\\"\" (TimeSpan.FromSeconds(20.0))\n\n                if listResult.ExitCode = Some 0 then\n                    let names =\n                        listResult.StdOut.Split([| '\\r'; '\\n' |], StringSplitOptions.RemoveEmptyEntries)\n                        |> Array.toList\n\n                    let matchesPrefix (name: string) =\n                        containerPrefixes\n                        |> List.exists (fun prefix ->\n                            name.Equals(prefix, StringComparison.OrdinalIgnoreCase)\n                            || name.StartsWith(prefix + \"-\", StringComparison.OrdinalIgnoreCase))\n\n                    for name in names do\n                        if matchesPrefix name then\n                            let! result = runProcessAsync \"docker\" $\"rm -f {name}\" (TimeSpan.FromSeconds(20.0))\n\n                            if result.ExitCode = Some 0 then\n                                if not (String.IsNullOrWhiteSpace result.StdOut) then\n                                    Console.WriteLine($\"Docker cleanup: removed {name}.\")\n                            else if result.Error.IsSome then\n                                Console.WriteLine($\"Docker cleanup ({name}): {result.Error.Value}\")\n                            else if not (String.IsNullOrWhiteSpace result.StdErr) then\n                                Console.WriteLine($\"Docker cleanup ({name}) stderr: {result.StdErr.Trim()}\")\n                else if listResult.Error.IsSome then\n                    Console.WriteLine($\"Docker cleanup list failed: {listResult.Error.Value}\")\n                else if not (String.IsNullOrWhiteSpace listResult.StdErr) then\n                    Console.WriteLine($\"Docker cleanup list stderr: {listResult.StdErr.Trim()}\")\n        }\n\n    let private formatLogTail (label: string) (lines: string list) (maxLines: int) =\n        let tail =\n            lines\n            |> List.rev\n            |> List.truncate maxLines\n            |> List.rev\n            |> String.concat Environment.NewLine\n\n        if String.IsNullOrWhiteSpace tail then\n            $\"{label}: <no logs captured>\"\n        else\n            $\"{label}:{Environment.NewLine}{tail}\"\n\n    let private getResourceLogSnapshotAsync (app: DistributedApplication) =\n        task {\n            let model = app.Services.GetRequiredService<DistributedApplicationModel>()\n            let tasks =\n                model.Resources\n                |> Seq.map (fun resource ->\n                    task {\n                        let name = resource.Name\n\n                        try\n                            let! logLines = getResourceLogsAsync app name\n                            return formatLogTail $\"[{name}]\" logLines 50\n                        with\n                        | ex ->\n                            return $\"[{name}]: failed to capture logs ({ex.Message})\"\n                    })\n                |> Seq.toArray\n\n            let! snapshots = Task.WhenAll(tasks)\n            return snapshots |> String.concat Environment.NewLine\n        }\n\n    let private formatProcessFailure (label: string) (result: ProcessResult) =\n        if result.TimedOut then\n            $\"{label} timed out.\"\n        else\n            let exitCode =\n                result.ExitCode\n                |> Option.map string\n                |> Option.defaultValue \"<unknown>\"\n\n            let details =\n                [\n                    if not (String.IsNullOrWhiteSpace result.StdOut) then\n                        $\"stdout:{Environment.NewLine}{result.StdOut.TrimEnd()}\"\n                    if not (String.IsNullOrWhiteSpace result.StdErr) then\n                        $\"stderr:{Environment.NewLine}{result.StdErr.TrimEnd()}\"\n                ]\n                |> String.concat Environment.NewLine\n\n            match result.Error with\n            | Some errorMessage -> $\"{label} failed: {errorMessage}\"\n            | None when not (String.IsNullOrWhiteSpace details) -> $\"{label} exited with {exitCode}.{Environment.NewLine}{details}\"\n            | None -> $\"{label} exited with {exitCode}.\"\n\n    let private tryGetDockerDiagnosticsAsync () =\n        task {\n            let! psResult = runProcessAsync \"docker\" \"ps -a --format \\\"{{.ID}} {{.Names}}\\\"\" (TimeSpan.FromSeconds(10.0))\n\n            if psResult.TimedOut || psResult.ExitCode <> Some 0 || psResult.Error.IsSome then\n                return formatProcessFailure \"Docker ps\" psResult\n            else\n                let lines =\n                    psResult.StdOut.Split([| '\\r'; '\\n' |], StringSplitOptions.RemoveEmptyEntries)\n\n                if lines.Length = 0 then\n                    return \"Docker ps: no containers.\"\n                else\n                    let containers =\n                        lines\n                        |> Seq.map (fun line ->\n                            let parts = line.Split([| ' ' |], 2, StringSplitOptions.RemoveEmptyEntries)\n                            if parts.Length = 0 then\n                                None\n                            else\n                                let id = parts[0]\n                                let name = if parts.Length > 1 then parts[1] else \"<unknown>\"\n                                Some(id, name))\n                        |> Seq.choose id\n                        |> Seq.truncate 10\n                        |> Seq.toArray\n\n                    let tasks =\n                        containers\n                        |> Array.map (fun (id, name) ->\n                            task {\n                                let! logResult = runProcessAsync \"docker\" $\"logs --tail 200 {id}\" (TimeSpan.FromSeconds(15.0))\n\n                                if logResult.TimedOut || logResult.ExitCode <> Some 0 || logResult.Error.IsSome then\n                                    return formatProcessFailure $\"Docker logs ({name})\" logResult\n                                else if String.IsNullOrWhiteSpace logResult.StdOut then\n                                    return $\"Docker logs ({name}): <empty>\"\n                                else\n                                    return $\"Docker logs ({name}):{Environment.NewLine}{logResult.StdOut.TrimEnd()}\"\n                            })\n\n                    let! logBlocks = Task.WhenAll(tasks)\n\n                    let containersSummary = String.Join(Environment.NewLine, lines)\n                    return $\"Docker containers:{Environment.NewLine}{containersSummary}{Environment.NewLine}{String.Join(Environment.NewLine, logBlocks)}\"\n        }\n\n    let private waitForResourceHealthyAsync\n        (notificationService: ResourceNotificationService)\n        (app: DistributedApplication)\n        (resourceName: string)\n        (ct: CancellationToken)\n        =\n        task {\n            try\n                let! _ = notificationService.WaitForResourceHealthyAsync(resourceName, ct)\n                ()\n            with\n            | ex ->\n                let details = describeResourceState notificationService resourceName\n                let! logLines = getResourceLogsAsync app resourceName\n\n                let lastLines =\n                    logLines\n                    |> List.rev\n                    |> List.truncate 50\n                    |> List.rev\n                    |> String.concat Environment.NewLine\n\n                let logDetails =\n                    if String.IsNullOrWhiteSpace lastLines then\n                        \"No logs captured.\"\n                    else\n                        $\"Last logs:{Environment.NewLine}{lastLines}\"\n\n                raise (Exception($\"{resourceName} failed to start. {details}{Environment.NewLine}{logDetails}\", ex))\n        }\n\n    let private requireEnv (resourceName: string) (key: string) (env: Map<string, string>) =\n        env\n        |> Map.tryFind key\n        |> Option.defaultWith (fun () -> failwith $\"Missing env var '{key}' for resource '{resourceName}'.\")\n\n    let private redactServiceBusConnectionString (connectionString: string) =\n        if String.IsNullOrWhiteSpace connectionString then\n            connectionString\n        else\n            connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries)\n            |> Array.map (fun part ->\n                if part.StartsWith(\"SharedAccessKey=\", StringComparison.OrdinalIgnoreCase) then\n                    \"SharedAccessKey=***\"\n                else\n                    part)\n            |> String.concat \";\"\n\n    let private redactStorageConnectionString (connectionString: string) =\n        if String.IsNullOrWhiteSpace connectionString then\n            connectionString\n        else\n            connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries)\n            |> Array.map (fun part ->\n                if part.StartsWith(\"AccountKey=\", StringComparison.OrdinalIgnoreCase) then\n                    \"AccountKey=***\"\n                else\n                    part)\n            |> String.concat \";\"\n\n    let private redactCosmosConnectionString (connectionString: string) =\n        if String.IsNullOrWhiteSpace connectionString then\n            connectionString\n        else\n            connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries)\n            |> Array.map (fun part ->\n                if part.StartsWith(\"AccountKey=\", StringComparison.OrdinalIgnoreCase) then\n                    \"AccountKey=***\"\n                else\n                    part)\n            |> String.concat \";\"\n\n    let private tryGetConnValue (prefix: string) (value: string) =\n        value.Split(';', StringSplitOptions.RemoveEmptyEntries)\n        |> Array.tryPick (fun segment ->\n            if segment.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) then\n                Some(segment.Substring(prefix.Length))\n            else\n                None)\n        |> Option.defaultValue \"<missing>\"\n\n    let private formatEnvDiagnostics (env: Map<string, string>) =\n        let get key =\n            match env |> Map.tryFind key with\n            | Some value when not (String.IsNullOrWhiteSpace value) -> value\n            | _ -> \"<missing>\"\n\n        [\n            Constants.EnvironmentVariables.GraceLogDirectory\n            Constants.EnvironmentVariables.OrleansClusterId\n            Constants.EnvironmentVariables.OrleansServiceId\n            Constants.EnvironmentVariables.DiffContainerName\n            Constants.EnvironmentVariables.DirectoryVersionContainerName\n            Constants.EnvironmentVariables.ZipFileContainerName\n            Constants.EnvironmentVariables.AzureCosmosDBConnectionString\n            Constants.EnvironmentVariables.AzureStorageConnectionString\n            Constants.EnvironmentVariables.AzureServiceBusConnectionString\n        ]\n        |> List.map (fun key ->\n            if key.Equals(Constants.EnvironmentVariables.AzureCosmosDBConnectionString, StringComparison.OrdinalIgnoreCase) then\n                let value =\n                    env\n                    |> Map.tryFind key\n                    |> Option.defaultValue String.Empty\n\n                $\"{key}={redactCosmosConnectionString value}\"\n            else if key.Equals(Constants.EnvironmentVariables.AzureStorageConnectionString, StringComparison.OrdinalIgnoreCase) then\n                let value =\n                    env\n                    |> Map.tryFind key\n                    |> Option.defaultValue String.Empty\n\n                $\"{key}={redactStorageConnectionString value}\"\n            else if key.Equals(Constants.EnvironmentVariables.AzureServiceBusConnectionString, StringComparison.OrdinalIgnoreCase) then\n                let value =\n                    env\n                    |> Map.tryFind key\n                    |> Option.defaultValue String.Empty\n\n                $\"{key}={redactServiceBusConnectionString value}\"\n            else\n                $\"{key}={get key}\")\n        |> String.concat \"; \"\n\n    let private tryGetLatestLogTail (logDirectory: string) =\n        try\n            if Directory.Exists(logDirectory) then\n                let latest =\n                    Directory.EnumerateFiles(logDirectory, \"*.log\")\n                    |> Seq.sortByDescending (fun path -> File.GetLastWriteTimeUtc(path))\n                    |> Seq.tryHead\n\n                match latest with\n                | None -> None\n                | Some path ->\n                    let lines = File.ReadAllLines(path)\n\n                    let tail =\n                        lines\n                        |> Seq.skip (max 0 (lines.Length - 50))\n                        |> String.concat Environment.NewLine\n\n                    Some($\"Latest Grace.Server log: {path}{Environment.NewLine}{tail}\")\n            else\n                None\n        with\n        | _ -> None\n\n    let private waitForServiceBusReadyAsync\n        (serviceBusEmulatorName: string)\n        (serviceBusSqlName: string)\n        (state: TestHostState)\n        =\n        task {\n            let sw = Stopwatch.StartNew()\n            let timeout = getTimeout (TimeSpan.FromSeconds(60.0)) (TimeSpan.FromMinutes(3.0))\n            let mutable lastError = String.Empty\n            let mutable attempt = 0\n            let mutable ready = false\n            let redactedConnectionString = redactServiceBusConnectionString state.ServiceBusConnectionString\n            let endpoint = tryGetConnValue \"Endpoint=\" state.ServiceBusConnectionString\n\n            Console.WriteLine(\n                $\"Service Bus readiness probe: Endpoint={endpoint}; Topic={state.ServiceBusTopic}; Subscription={state.ServiceBusTestSubscription}; Connection={redactedConnectionString}\"\n            )\n\n            let getResourceDiagnosticsAsync (resourceName: string) =\n                task {\n                    let notificationService =\n                        state.App.Services.GetRequiredService<ResourceNotificationService>()\n\n                    let details = describeResourceState notificationService resourceName\n\n                    let! logDetails =\n                        task {\n                            try\n                                let! logLines = getResourceLogsAsync state.App resourceName\n                                return formatLogTail $\"{resourceName} logs\" logLines 50\n                            with\n                            | ex ->\n                                return $\"{resourceName} logs: <failed to capture ({ex.Message})>\"\n                        }\n\n                    return $\"{resourceName}: {details}{Environment.NewLine}{logDetails}\"\n                }\n\n            while not ready do\n                if sw.Elapsed >= timeout then\n                    let! emulatorDetails = getResourceDiagnosticsAsync serviceBusEmulatorName\n                    let! sqlDetails = getResourceDiagnosticsAsync serviceBusSqlName\n                    let! dockerDetails = tryGetDockerDiagnosticsAsync ()\n\n                    raise (\n                        TimeoutException(\n                            $\"Timed out waiting for Service Bus emulator. Attempts={attempt}; Elapsed={sw.Elapsed.TotalSeconds:n1}s; Last error: {lastError}{Environment.NewLine}{emulatorDetails}{Environment.NewLine}{sqlDetails}{Environment.NewLine}{dockerDetails}\"\n                        )\n                    )\n\n                attempt <- attempt + 1\n\n                try\n                    let client = ServiceBusClient(state.ServiceBusConnectionString)\n                    use _client = client\n\n                    let receiver =\n                        client.CreateReceiver(\n                            state.ServiceBusTopic,\n                            state.ServiceBusTestSubscription,\n                            ServiceBusReceiverOptions(ReceiveMode = ServiceBusReceiveMode.PeekLock)\n                        )\n\n                    use _receiver = receiver\n\n                    let! _ = receiver.PeekMessageAsync()\n                    ready <- true\n                with\n                | ex ->\n                    lastError <- ex.Message\n                    Console.WriteLine($\"Service Bus readiness attempt {attempt} failed: {lastError}\")\n                    do! Task.Delay(TimeSpan.FromSeconds(1.0))\n        }\n\n    let private waitForCosmosReadyAsync (connectionString: string) (databaseName: string) (containerName: string) =\n        task {\n            if String.IsNullOrWhiteSpace connectionString then\n                return ()\n            else if String.IsNullOrWhiteSpace databaseName\n                    || String.IsNullOrWhiteSpace containerName then\n                return ()\n            else\n                let isLocalCosmos =\n                    connectionString.Contains(\"localhost\", StringComparison.OrdinalIgnoreCase)\n                    || connectionString.Contains(\"127.0.0.1\", StringComparison.OrdinalIgnoreCase)\n\n                let handler =\n                    new SocketsHttpHandler(\n                        SslOptions =\n                            new SslClientAuthenticationOptions(TargetHost = \"localhost\", RemoteCertificateValidationCallback = (fun _ __ ___ ____ -> true))\n                    )\n\n                let options = CosmosClientOptions(ConnectionMode = ConnectionMode.Gateway, LimitToEndpoint = true)\n                options.RequestTimeout <- TimeSpan.FromSeconds(10.0)\n                options.HttpClientFactory <- (fun () -> new HttpClient(handler, disposeHandler = true))\n\n                use client = new CosmosClient(connectionString, options)\n                let sw = Stopwatch.StartNew()\n                let timeout = getTimeout (TimeSpan.FromMinutes(3.0)) (TimeSpan.FromMinutes(3.0))\n                let perCallTimeout = TimeSpan.FromSeconds(10.0)\n                let mutable lastError = String.Empty\n                let mutable attempt = 0\n                let mutable ready = false\n\n                while not ready do\n                    if sw.Elapsed >= timeout then\n                        let diagnostics =\n                            $\"Timed out waiting for Cosmos emulator. Attempts={attempt}. LastError={lastError}. Database={databaseName}. Container={containerName}. ConnectionString={redactCosmosConnectionString connectionString}\"\n\n                        raise (TimeoutException(diagnostics))\n\n                    attempt <- attempt + 1\n\n                    try\n                        let! _ =\n                            client\n                                .ReadAccountAsync()\n                                .WaitAsync(perCallTimeout)\n\n                        if isLocalCosmos then\n                            let! database =\n                                client\n                                    .CreateDatabaseIfNotExistsAsync(databaseName)\n                                    .WaitAsync(perCallTimeout)\n\n                            let! _ =\n                                database.Database\n                                    .CreateContainerIfNotExistsAsync(containerName, \"/PartitionKey\")\n                                    .WaitAsync(perCallTimeout)\n                            ()\n\n                        ready <- true\n                    with\n                    | ex ->\n                        lastError <- ex.Message\n                        do! Task.Delay(TimeSpan.FromSeconds(1.0))\n        }\n\n    let private waitForGraceServerHttpReadyAsync (client: HttpClient) (ct: CancellationToken) =\n        task {\n            let perRequestTimeout = getTimeout (TimeSpan.FromSeconds(10.0)) (TimeSpan.FromSeconds(20.0))\n            let sw = Stopwatch.StartNew()\n            let mutable attempt = 0\n            let mutable lastError = String.Empty\n            let delayAsync () =\n                task {\n                    try\n                        do! Task.Delay(TimeSpan.FromSeconds(1.0), ct)\n                    with\n                    | :? OperationCanceledException when ct.IsCancellationRequested ->\n                        raise (\n                            TimeoutException(\n                                $\"Timed out waiting for Grace.Server HTTP readiness. Last error: {lastError}\"\n                            )\n                        )\n                }\n\n            Console.WriteLine($\"Waiting for Grace.Server HTTP readiness at {client.BaseAddress}...\")\n\n            let mutable ready = false\n\n            while not ready do\n                if ct.IsCancellationRequested then\n                    raise (\n                        TimeoutException($\"Timed out waiting for Grace.Server HTTP readiness. Last error: {lastError}\")\n                    )\n\n                attempt <- attempt + 1\n\n                try\n                    use linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct)\n                    linkedCts.CancelAfter(perRequestTimeout)\n\n                    use! response = client.GetAsync(\"/healthz\", linkedCts.Token)\n\n                    if response.IsSuccessStatusCode then\n                        Console.WriteLine(\n                            $\"Grace.Server HTTP readiness confirmed after {sw.Elapsed.TotalSeconds:n1}s (attempt {attempt}).\"\n                        )\n                        ready <- true\n                    else\n                        lastError <- $\"Status {(int response.StatusCode)} {response.StatusCode}\"\n                        do! delayAsync ()\n                with\n                | :? OperationCanceledException when ct.IsCancellationRequested ->\n                    raise (\n                        TimeoutException($\"Timed out waiting for Grace.Server HTTP readiness. Last error: {lastError}\")\n                    )\n                | ex ->\n                    lastError <- ex.Message\n                    do! delayAsync ()\n        }\n\n    let private startNewHostAsync (bootstrapUserId: string) =\n        task {\n            Environment.SetEnvironmentVariable(\"GRACE_TESTING\", \"1\")\n            Environment.SetEnvironmentVariable(\"GRACE_TEST_CLEANUP\", \"1\")\n            Environment.SetEnvironmentVariable(\"GRACE_TEST_RUN_ID\", Guid.NewGuid().ToString(\"N\"))\n            Environment.SetEnvironmentVariable(\"ASPIRE_RESOURCE_MODE\", \"Local\")\n            Environment.SetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthOidcAuthority, \"https://auth.grace.test\")\n            Environment.SetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthOidcAudience, \"https://api.grace.test\")\n            Environment.SetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthOidcCliClientId, \"grace-cli-test-client\")\n\n            if not <| String.IsNullOrWhiteSpace bootstrapUserId then\n                Environment.SetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthzBootstrapSystemAdminUsers, bootstrapUserId)\n            do! cleanupDockerContainersAsync ()\n            let! builder = DistributedApplicationTestingBuilder.CreateAsync<Projects.Grace_Aspire_AppHost>()\n            let! app = builder.BuildAsync()\n            do! app.StartAsync()\n\n            let notificationService = app.Services.GetRequiredService<ResourceNotificationService>()\n            let model = app.Services.GetRequiredService<DistributedApplicationModel>()\n\n            for resource in model.Resources do\n                Console.WriteLine($\"Resource: {resource.Name} ({resource.GetType().Name})\")\n\n                match resource with\n                | :? IResourceWithEndpoints as resourceWithEndpoints ->\n                    for endpoint in resourceWithEndpoints.GetEndpoints() do\n                        let endpointName = endpoint.EndpointAnnotation.Name\n                        let endpointUri = app.GetEndpoint(resource.Name, endpointName)\n                        Console.WriteLine($\"  Endpoint: {resource.Name}/{endpointName} -> {endpointUri}\")\n                | _ -> ()\n\n            use cts = new CancellationTokenSource(defaultWaitTimeout)\n\n            match tryFindResourceByName app azuriteResourceName with\n            | Some _ ->\n                Console.WriteLine($\"Azurite resource detected: {azuriteResourceName}\")\n                do! waitForResourceHealthyAsync notificationService app azuriteResourceName cts.Token\n            | None -> Console.WriteLine(\"Azurite resource not found in model.\")\n\n            let serviceBusSqlResourceName = getServiceBusSqlResourceName ()\n            let serviceBusEmulatorResourceName = getServiceBusEmulatorResourceName ()\n\n            if not (shouldSkipServiceBus ()) then\n                match tryFindResourceByName app serviceBusSqlResourceName with\n                | Some _ ->\n                    Console.WriteLine($\"Service Bus SQL resource detected: {serviceBusSqlResourceName}\")\n                    do! waitForResourceHealthyAsync notificationService app serviceBusSqlResourceName cts.Token\n                | None -> Console.WriteLine(\"Service Bus SQL resource not found in model.\")\n            else\n                Console.WriteLine(\"Skipping Service Bus SQL readiness checks (GRACE_TEST_SKIP_SERVICEBUS=1).\")\n\n            match tryFindResourceName<AzureCosmosDBEmulatorResource> app with\n            | Some name ->\n                Console.WriteLine($\"Cosmos emulator resource detected: {name}\")\n                do! waitForResourceHealthyAsync notificationService app name cts.Token\n            | None -> Console.WriteLine(\"Cosmos emulator resource not found in model.\")\n\n            if not (shouldSkipServiceBus ()) then\n                do! waitForResourceHealthyAsync notificationService app serviceBusEmulatorResourceName cts.Token\n            else\n                Console.WriteLine(\"Skipping Service Bus emulator readiness checks (GRACE_TEST_SKIP_SERVICEBUS=1).\")\n            let! env = getEnvironmentVariablesAsync app graceServerResourceName\n\n            match env\n                  |> Map.tryFind Constants.EnvironmentVariables.GraceLogDirectory\n                with\n            | Some value when not (String.IsNullOrWhiteSpace value) -> Console.WriteLine($\"GraceLogDirectory: {value}\")\n            | _ -> Console.WriteLine(\"GraceLogDirectory: <missing>\")\n\n            match env\n                  |> Map.tryFind Constants.EnvironmentVariables.DebugEnvironment\n                with\n            | Some value when not (String.IsNullOrWhiteSpace value) -> Console.WriteLine($\"DebugEnvironment: {value}\")\n            | _ -> Console.WriteLine(\"DebugEnvironment: <missing>\")\n\n            match env\n                  |> Map.tryFind Constants.EnvironmentVariables.AzureStorageConnectionString\n                with\n            | Some value when not (String.IsNullOrWhiteSpace value) -> Console.WriteLine($\"AzureStorageConnectionString: {redactStorageConnectionString value}\")\n            | _ -> Console.WriteLine(\"AzureStorageConnectionString: <missing>\")\n\n            match env\n                  |> Map.tryFind Constants.EnvironmentVariables.AzureServiceBusConnectionString\n                with\n            | Some value when not (String.IsNullOrWhiteSpace value) ->\n                Console.WriteLine($\"AzureServiceBusConnectionString: {redactServiceBusConnectionString value}\")\n            | _ -> Console.WriteLine(\"AzureServiceBusConnectionString: <missing>\")\n\n            match env\n                  |> Map.tryFind Constants.EnvironmentVariables.AzureCosmosDBConnectionString\n                with\n            | Some value when not (String.IsNullOrWhiteSpace value) -> Console.WriteLine($\"AzureCosmosDBConnectionString: {redactCosmosConnectionString value}\")\n            | _ -> Console.WriteLine(\"AzureCosmosDBConnectionString: <missing>\")\n\n            let cosmosConnectionString =\n                env\n                |> Map.tryFind Constants.EnvironmentVariables.AzureCosmosDBConnectionString\n                |> Option.defaultValue String.Empty\n\n            let cosmosDatabaseName =\n                env\n                |> Map.tryFind Constants.EnvironmentVariables.AzureCosmosDBDatabaseName\n                |> Option.defaultValue String.Empty\n\n            let cosmosContainerName =\n                env\n                |> Map.tryFind Constants.EnvironmentVariables.AzureCosmosDBContainerName\n                |> Option.defaultValue String.Empty\n\n            do! waitForCosmosReadyAsync cosmosConnectionString cosmosDatabaseName cosmosContainerName\n\n            try\n                do! waitForResourceHealthyAsync notificationService app graceServerResourceName cts.Token\n            with\n            | ex ->\n                let details = describeResourceState notificationService graceServerResourceName\n                let! logLines = getResourceLogsAsync app graceServerResourceName\n                let envDetails = formatEnvDiagnostics env\n\n                let graceLogDetails =\n                    env\n                    |> Map.tryFind Constants.EnvironmentVariables.GraceLogDirectory\n                    |> Option.bind tryGetLatestLogTail\n                    |> Option.defaultValue \"No Grace.Server log file captured.\"\n\n                let lastLines =\n                    logLines\n                    |> List.rev\n                    |> List.truncate 20\n                    |> List.rev\n                    |> String.concat Environment.NewLine\n\n                let logDetails =\n                    if String.IsNullOrWhiteSpace lastLines then\n                        \"No grace-server logs captured.\"\n                    else\n                        $\"Last grace-server logs:{Environment.NewLine}{lastLines}\"\n\n                raise (\n                    Exception(\n                        $\"Grace-server failed to start. {details}{Environment.NewLine}Env: {envDetails}{Environment.NewLine}{logDetails}{Environment.NewLine}{graceLogDetails}\",\n                        ex\n                    )\n                )\n\n            let endpointName = getEndpointName app graceServerResourceName\n            let endpointUri = app.GetEndpoint(graceServerResourceName, endpointName)\n            let client = app.CreateHttpClient(graceServerResourceName, endpointName)\n            client.Timeout <- getTimeout (TimeSpan.FromSeconds(100.0)) (TimeSpan.FromMinutes(5.0))\n\n            try\n                do! waitForGraceServerHttpReadyAsync client cts.Token\n            with\n            | ex ->\n                let details = describeResourceState notificationService graceServerResourceName\n                let! graceResourceLogs = getResourceLogsAsync app graceServerResourceName\n                let graceResourceLogDetails = formatLogTail \"Grace.Server resource logs\" graceResourceLogs 50\n                let envDetails = formatEnvDiagnostics env\n\n                let graceFileLog =\n                    env\n                    |> Map.tryFind Constants.EnvironmentVariables.GraceLogDirectory\n                    |> Option.bind tryGetLatestLogTail\n                    |> Option.defaultValue \"No Grace.Server log file captured.\"\n\n                let! aspireLogSnapshot = getResourceLogSnapshotAsync app\n                let! dockerDiagnostics = tryGetDockerDiagnosticsAsync ()\n\n                raise (\n                    Exception(\n                        $\"Grace-server HTTP readiness failed. {details}{Environment.NewLine}Error: {ex.Message}{Environment.NewLine}Env: {envDetails}{Environment.NewLine}{graceResourceLogDetails}{Environment.NewLine}{graceFileLog}{Environment.NewLine}{aspireLogSnapshot}{Environment.NewLine}{dockerDiagnostics}\",\n                        ex\n                    )\n                )\n\n            let diagnosticsPath = Path.Combine(Path.GetTempPath(), \"grace-server-tests.host.log\")\n\n            let tryGetConnValue (prefix: string) (value: string) =\n                value.Split(';', StringSplitOptions.RemoveEmptyEntries)\n                |> Array.tryPick (fun segment ->\n                    if segment.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) then\n                        Some(segment.Substring(prefix.Length))\n                    else\n                        None)\n                |> Option.defaultValue \"<missing>\"\n\n            let cosmosConn =\n                env\n                |> Map.tryFind Constants.EnvironmentVariables.AzureCosmosDBConnectionString\n                |> Option.defaultValue String.Empty\n\n            let cosmosEndpoint = tryGetConnValue \"AccountEndpoint=\" cosmosConn\n            let cosmosConnRedacted = redactCosmosConnectionString cosmosConn\n\n            let serviceBusConn =\n                env\n                |> Map.tryFind Constants.EnvironmentVariables.AzureServiceBusConnectionString\n                |> Option.defaultValue String.Empty\n\n            let serviceBusEndpoint = tryGetConnValue \"Endpoint=\" serviceBusConn\n\n            let diagnosticsLine =\n                $\"GraceServer endpoint={endpointUri}; EndpointName={endpointName}; UsingBase={client.BaseAddress}; CosmosEndpoint={cosmosEndpoint}; CosmosConn={cosmosConnRedacted}; ServiceBusEndpoint={serviceBusEndpoint}\"\n\n            File.AppendAllText(diagnosticsPath, diagnosticsLine + Environment.NewLine)\n\n            let serviceBusConnectionString, serviceBusTopic, serviceBusSubscription, serviceBusTestSubscription =\n                if shouldSkipServiceBus () then\n                    \"\", \"\", \"\", \"\"\n                else\n                    let connection =\n                        requireEnv graceServerResourceName Constants.EnvironmentVariables.AzureServiceBusConnectionString env\n\n                    let topic = requireEnv graceServerResourceName Constants.EnvironmentVariables.AzureServiceBusTopic env\n\n                    let subscription =\n                        requireEnv graceServerResourceName Constants.EnvironmentVariables.AzureServiceBusSubscription env\n\n                    let testSubscription = $\"{subscription}-tests\"\n                    connection, topic, subscription, testSubscription\n\n            let baseAddress = endpointUri.ToString().TrimEnd('/')\n\n            let state =\n                {\n                    App = app\n                    Client = client\n                    GraceServerBaseAddress = baseAddress\n                    ServiceBusConnectionString = serviceBusConnectionString\n                    ServiceBusTopic = serviceBusTopic\n                    ServiceBusServerSubscription = serviceBusSubscription\n                    ServiceBusTestSubscription = serviceBusTestSubscription\n                }\n\n            if not (shouldSkipServiceBus ()) then\n                do! waitForServiceBusReadyAsync serviceBusEmulatorResourceName serviceBusSqlResourceName state\n            else\n                Console.WriteLine(\"Skipping Service Bus functional readiness checks (GRACE_TEST_SKIP_SERVICEBUS=1).\")\n\n            return state\n        }\n\n    let startAsync (bootstrapUserId: string) =\n        task {\n            do! sharedStateLock.WaitAsync()\n\n            try\n                match sharedState with\n                | Some state ->\n                    ensureBootstrapCompatible bootstrapUserId\n                    return state\n                | None ->\n                    let! state = startNewHostAsync bootstrapUserId\n                    sharedBootstrapUserId <- Some bootstrapUserId\n                    sharedState <- Some state\n                    return state\n            finally\n                sharedStateLock.Release() |> ignore\n        }\n\n    let stopAsync (app: DistributedApplication option) =\n        task {\n            match app with\n            | None -> ()\n            | Some appHost ->\n                Console.WriteLine(\"Stopping Aspire host...\")\n                Console.WriteLine(\"Aspire host shutdown skipped to avoid test host teardown crashes.\")\n        }\n\n    let private createServiceBusReceiver (state: TestHostState) =\n        let options = ServiceBusReceiverOptions(ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete)\n        let client = ServiceBusClient(state.ServiceBusConnectionString)\n        let receiver = client.CreateReceiver(state.ServiceBusTopic, state.ServiceBusTestSubscription, options)\n        client, receiver\n\n    let drainServiceBusAsync (state: TestHostState) =\n        task {\n            let client, receiver = createServiceBusReceiver state\n            use _client = client\n            use _receiver = receiver\n\n            let mutable drainedCount = 0\n            let mutable keepGoing = true\n\n            while keepGoing do\n                let! message = receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(1.0))\n\n                if isNull message then keepGoing <- false else drainedCount <- drainedCount + 1\n\n            return drainedCount\n        }\n\n    let waitForOwnerCreatedEventAsync (state: TestHostState) (ownerId: string) =\n        task {\n            let client, receiver = createServiceBusReceiver state\n            use _client = client\n            use _receiver = receiver\n\n            let sw = Stopwatch.StartNew()\n            let timeout = TimeSpan.FromSeconds(30.0)\n            let lastBodies = ResizeArray<string>()\n            let maxBodies = 5\n\n            let mutable parsedOwnerId = Guid.Empty\n            let hasParsedOwnerId = Guid.TryParse(ownerId, &parsedOwnerId)\n            let mutable found: ServiceBusReceivedMessage option = None\n\n            while found.IsNone do\n                let remaining = timeout - sw.Elapsed\n\n                if remaining <= TimeSpan.Zero then\n                    let diagnostics =\n                        StringBuilder()\n                            .AppendLine(\"Timed out waiting for Owner Created event on Service Bus test subscription.\")\n                            .AppendLine($\"GraceServerBaseAddress: {state.GraceServerBaseAddress}\")\n                            .AppendLine($\"ServiceBusTopic: {state.ServiceBusTopic}\")\n                            .AppendLine($\"ServiceBusSubscription: {state.ServiceBusServerSubscription}\")\n                            .AppendLine($\"ServiceBusTestSubscription: {state.ServiceBusTestSubscription}\")\n                            .AppendLine($\"ServiceBusConnectionString: {redactServiceBusConnectionString state.ServiceBusConnectionString}\")\n                            .AppendLine($\"LastMessageBodies({lastBodies.Count}):\")\n                            .AppendLine(String.Join(Environment.NewLine, lastBodies))\n                            .ToString()\n\n                    raise (TimeoutException(diagnostics))\n\n                let waitTime =\n                    if remaining < TimeSpan.FromSeconds(2.0) then\n                        remaining\n                    else\n                        TimeSpan.FromSeconds(2.0)\n\n                let! message = receiver.ReceiveMessageAsync(waitTime)\n\n                if not (isNull message) then\n                    let body = message.Body.ToString()\n\n                    if lastBodies.Count >= maxBodies then lastBodies.RemoveAt(0)\n\n                    lastBodies.Add(body)\n\n                    let tryMatchOwnerCreated (ownerEvent: Owner.OwnerEvent) =\n                        match ownerEvent.Event with\n                        | Owner.OwnerEventType.Created (createdOwnerId, _) when hasParsedOwnerId && createdOwnerId = parsedOwnerId -> true\n                        | _ -> false\n\n                    let matchesOwnerCreated =\n                        try\n                            match JsonSerializer.Deserialize<Events.GraceEvent>(body, Constants.JsonSerializerOptions) with\n                            | Events.GraceEvent.OwnerEvent ownerEvent -> tryMatchOwnerCreated ownerEvent\n                            | _ -> false\n                        with\n                        | _ ->\n                            try\n                                let ownerEvent = JsonSerializer.Deserialize<Owner.OwnerEvent>(body, Constants.JsonSerializerOptions)\n                                tryMatchOwnerCreated ownerEvent\n                            with\n                            | _ -> false\n\n                    if matchesOwnerCreated then found <- Some message\n\n            return found.Value\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/Auth.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server.Tests.Services\nopen Grace.Server.Security\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.PersonalAccessToken\nopen Grace.Types.Auth\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Headers\nopen System.Threading.Tasks\n\n[<Parallelizable(ParallelScope.All)>]\ntype AuthInfo = { GraceUserId: string; Claims: string list; RawClaims: (string * string) list }\n\n[<Parallelizable(ParallelScope.All)>]\ntype AuthEndpoints() =\n\n    [<Test>]\n    member _.LoginPageShowsNoProvidersWhenNotConfigured() =\n        task {\n            let! response = Client.GetAsync(\"/auth/login\")\n            response.EnsureSuccessStatusCode() |> ignore\n            let! body = response.Content.ReadAsStringAsync()\n            Assert.That(body, Does.Contain(\"Interactive browser login is not available on the server in this phase.\"))\n        }\n\n    [<Test>]\n    member _.LoginProviderReturnsNotFoundWhenNotConfigured() =\n        task {\n            let! response = Client.GetAsync(\"/auth/login/microsoft\")\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound))\n        }\n\n    [<Test>]\n    member _.AuthOidcConfigReturnsConfiguredValues() =\n        task {\n            let rawAuthority = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthOidcAuthority)\n\n            let rawAudience = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthOidcAudience)\n\n            let rawClientId = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.GraceAuthOidcCliClientId)\n\n            Assert.That(rawAuthority, Is.Not.Null.And.Not.Empty)\n            Assert.That(rawAudience, Is.Not.Null.And.Not.Empty)\n            Assert.That(rawClientId, Is.Not.Null.And.Not.Empty)\n\n            let normalizeAuthority (value: string) =\n                let trimmed = value.Trim()\n\n                if trimmed.EndsWith(\"/\", StringComparison.Ordinal) then\n                    trimmed\n                else\n                    $\"{trimmed}/\"\n\n            use unauthenticatedClient = new HttpClient()\n            unauthenticatedClient.BaseAddress <- Client.BaseAddress\n\n            let! response = unauthenticatedClient.GetAsync(\"/auth/oidc/config\")\n            response.EnsureSuccessStatusCode() |> ignore\n\n            let! returnValue = deserializeContent<GraceReturnValue<OidcClientConfig>> response\n\n            Assert.That(returnValue.ReturnValue.Authority, Is.EqualTo(normalizeAuthority rawAuthority))\n            Assert.That(returnValue.ReturnValue.Audience, Is.EqualTo(rawAudience.Trim()))\n            Assert.That(returnValue.ReturnValue.CliClientId, Is.EqualTo(rawClientId.Trim()))\n        }\n\n    [<Test>]\n    member _.AuthMeReturnsGraceUserId() =\n        task {\n            let! response = Client.GetAsync(\"/auth/me\")\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<AuthInfo>> response\n            Assert.That(returnValue.ReturnValue.GraceUserId, Is.EqualTo(testUserId))\n            Assert.That(returnValue.ReturnValue.Claims, Is.Empty)\n            Assert.That(returnValue.ReturnValue.RawClaims, Has.Some.EqualTo((PrincipalMapper.GraceUserIdClaim, testUserId)))\n        }\n\n    [<Test>]\n    member _.AuthMeRequiresAuthentication() =\n        task {\n            use unauthenticatedClient = new HttpClient()\n            unauthenticatedClient.BaseAddress <- Client.BaseAddress\n            let! response = unauthenticatedClient.GetAsync(\"/auth/me\")\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n        }\n\n    [<Test>]\n    member _.AuthTokenCreateAndUse() =\n        task {\n            let parameters = Grace.Shared.Parameters.Auth.CreatePersonalAccessTokenParameters()\n            parameters.TokenName <- $\"pat-{System.Guid.NewGuid():N}\"\n            let! response = Client.PostAsync(\"/auth/token/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n\n            let! returnValue = deserializeContent<GraceReturnValue<PersonalAccessTokenCreated>> response\n            let token = returnValue.ReturnValue.Token\n\n            use patClient = new HttpClient()\n            patClient.BaseAddress <- Client.BaseAddress\n            patClient.DefaultRequestHeaders.Authorization <- AuthenticationHeaderValue(\"Bearer\", token)\n\n            let! meResponse = patClient.GetAsync(\"/auth/me\")\n            meResponse.EnsureSuccessStatusCode() |> ignore\n\n            let! meValue = deserializeContent<GraceReturnValue<AuthInfo>> meResponse\n            Assert.That(meValue.ReturnValue.GraceUserId, Is.EqualTo(testUserId))\n        }\n\n    [<Test>]\n    member _.AuthTokenRevokeBlocksAccess() =\n        task {\n            let parameters = Grace.Shared.Parameters.Auth.CreatePersonalAccessTokenParameters()\n            parameters.TokenName <- $\"pat-{System.Guid.NewGuid():N}\"\n            let! response = Client.PostAsync(\"/auth/token/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<PersonalAccessTokenCreated>> response\n            let created = returnValue.ReturnValue\n\n            let revokeParameters = Grace.Shared.Parameters.Auth.RevokePersonalAccessTokenParameters()\n            revokeParameters.TokenId <- created.Summary.TokenId\n            let! revokeResponse = Client.PostAsync(\"/auth/token/revoke\", createJsonContent revokeParameters)\n            revokeResponse.EnsureSuccessStatusCode() |> ignore\n\n            use patClient = new HttpClient()\n            patClient.BaseAddress <- Client.BaseAddress\n            patClient.DefaultRequestHeaders.Authorization <- AuthenticationHeaderValue(\"Bearer\", created.Token)\n            let! meResponse = patClient.GetAsync(\"/auth/me\")\n            Assert.That(meResponse.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n        }\n\n    [<Test>]\n    member _.AuthTokenMaxLifetimeEnforced() =\n        task {\n            let parameters = Grace.Shared.Parameters.Auth.CreatePersonalAccessTokenParameters()\n            parameters.TokenName <- $\"pat-{System.Guid.NewGuid():N}\"\n            parameters.ExpiresInSeconds <- 400L * 86400L\n            let! response = Client.PostAsync(\"/auth/token/create\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n        }\n\n    [<Test>]\n    member _.AuthTokenListIncludesCreated() =\n        task {\n            let parameters = Grace.Shared.Parameters.Auth.CreatePersonalAccessTokenParameters()\n            parameters.TokenName <- $\"pat-{System.Guid.NewGuid():N}\"\n            let! response = Client.PostAsync(\"/auth/token/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! createdReturn = deserializeContent<GraceReturnValue<PersonalAccessTokenCreated>> response\n\n            let listParameters = Grace.Shared.Parameters.Auth.ListPersonalAccessTokensParameters()\n            let! listResponse = Client.PostAsync(\"/auth/token/list\", createJsonContent listParameters)\n            listResponse.EnsureSuccessStatusCode() |> ignore\n            let! listReturn = deserializeContent<GraceReturnValue<PersonalAccessTokenSummary list>> listResponse\n\n            let createdId = createdReturn.ReturnValue.Summary.TokenId\n\n            let containsToken =\n                listReturn.ReturnValue\n                |> List.exists (fun token -> token.TokenId = createdId)\n\n            Assert.That(containsToken, Is.True)\n\n            let revokeParameters = Grace.Shared.Parameters.Auth.RevokePersonalAccessTokenParameters()\n            revokeParameters.TokenId <- createdId\n            let! revokeResponse = Client.PostAsync(\"/auth/token/revoke\", createJsonContent revokeParameters)\n            revokeResponse.EnsureSuccessStatusCode() |> ignore\n\n            let listWithRevoked = Grace.Shared.Parameters.Auth.ListPersonalAccessTokensParameters()\n            listWithRevoked.IncludeRevoked <- true\n            let! revokedResponse = Client.PostAsync(\"/auth/token/list\", createJsonContent listWithRevoked)\n\n            revokedResponse.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! revokedReturn = deserializeContent<GraceReturnValue<PersonalAccessTokenSummary list>> revokedResponse\n\n            let revokedToken =\n                revokedReturn.ReturnValue\n                |> List.tryFind (fun token -> token.TokenId = createdId)\n\n            Assert.That(revokedToken.IsSome, Is.True)\n            Assert.That(revokedToken.Value.RevokedAt.IsSome, Is.True)\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/AuthMapping.Unit.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server.Security\nopen NUnit.Framework\nopen System.Security.Claims\n\n[<Parallelizable(ParallelScope.All)>]\ntype ClaimMappingTests() =\n    let createPrincipal (claims: Claim list) =\n        let identity = ClaimsIdentity(claims, \"Bearer\")\n        ClaimsPrincipal(identity)\n\n    let findValues (claimType: string) (claims: Claim list) =\n        claims\n        |> List.filter (fun claim -> claim.Type = claimType)\n        |> List.map (fun claim -> claim.Value)\n        |> Set.ofList\n\n    [<Test>]\n    member _.MapsGraceUserIdFromSubject() =\n        let principal = createPrincipal [ Claim(\"sub\", \"subject-1\") ]\n\n        let mapped = ClaimMapping.mapClaims principal\n        let userIds = findValues PrincipalMapper.GraceUserIdClaim mapped\n        Assert.That(userIds, Is.EquivalentTo([ \"subject-1\" ]))\n\n    [<Test>]\n    member _.DoesNotOverrideExistingGraceUserId() =\n        let principal =\n            createPrincipal [ Claim(PrincipalMapper.GraceUserIdClaim, \"existing-user\")\n                              Claim(\"tid\", \"tenant-2\")\n                              Claim(\"oid\", \"object-2\") ]\n\n        let mapped = ClaimMapping.mapClaims principal\n        let userIds = findValues PrincipalMapper.GraceUserIdClaim mapped\n        Assert.That(userIds.Count, Is.EqualTo(0))\n\n    [<Test>]\n    member _.MapsRolesScopesPermissionsAndGroups() =\n        let principal =\n            createPrincipal [ Claim(\"roles\", \"Admin\")\n                              Claim(\"scp\", \"repo.write repo.read\")\n                              Claim(\"scope\", \"repo.list\")\n                              Claim(\"permissions\", \"repo.delete\")\n                              Claim(\"groups\", \"group-1\") ]\n\n        let mapped = ClaimMapping.mapClaims principal\n        let graceClaims = findValues PrincipalMapper.GraceClaim mapped\n        let graceGroups = findValues PrincipalMapper.GraceGroupIdClaim mapped\n\n        Assert.That(\n            graceClaims,\n            Is.EquivalentTo(\n                [\n                    \"Admin\"\n                    \"repo.write\"\n                    \"repo.read\"\n                    \"repo.list\"\n                    \"repo.delete\"\n                ]\n            )\n        )\n\n        Assert.That(graceGroups, Is.EquivalentTo([ \"group-1\" ]))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Authorization.Unit.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Shared.Authorization\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype AuthorizationUnit() =\n\n    let roleCatalog = RoleCatalog.getAll ()\n\n    let createAssignment principal scope roleId =\n        { Principal = principal; Scope = scope; RoleId = roleId; Source = \"test\"; SourceDetail = None; CreatedAt = getCurrentInstant () }\n\n    let assertAllowed result =\n        match result with\n        | Allowed _ -> ()\n        | Denied reason -> Assert.Fail($\"Expected Allowed but got Denied: {reason}\")\n\n    let assertDenied result =\n        match result with\n        | Denied _ -> ()\n        | Allowed reason -> Assert.Fail($\"Expected Denied but got Allowed: {reason}\")\n\n    [<Test>]\n    member _.RoleInheritanceAllowsRepoWriteFromOrgAdmin() =\n        let ownerId = Guid.NewGuid()\n        let organizationId = Guid.NewGuid()\n        let repositoryId = Guid.NewGuid()\n\n        let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-1\" }\n\n        let assignments =\n            [\n                createAssignment principal (Scope.Organization(ownerId, organizationId)) \"OrgAdmin\"\n            ]\n\n        let result =\n            checkPermission roleCatalog assignments [] [ principal ] Set.empty Operation.RepoWrite (Resource.Repository(ownerId, organizationId, repositoryId))\n\n        assertAllowed result\n\n    [<Test>]\n    member _.PathPermissionDenyOverridesRoleAllow() =\n        let ownerId = Guid.NewGuid()\n        let organizationId = Guid.NewGuid()\n        let repositoryId = Guid.NewGuid()\n\n        let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-2\" }\n\n        let assignments =\n            [\n                createAssignment principal (Scope.Organization(ownerId, organizationId)) \"OrgAdmin\"\n            ]\n\n        let denyPermissions = List<ClaimPermission>()\n        denyPermissions.Add({ Claim = \"engineering\"; DirectoryPermission = DirectoryPermission.NoAccess })\n\n        let pathPermissions =\n            [\n                { Path = \"/images\"; Permissions = denyPermissions }\n            ]\n\n        let result =\n            checkPermission\n                roleCatalog\n                assignments\n                pathPermissions\n                [ principal ]\n                (Set.ofList [ \"engineering\" ])\n                Operation.PathWrite\n                (Resource.Path(ownerId, organizationId, repositoryId, \"/images\"))\n\n        assertDenied result\n\n    [<Test>]\n    member _.PathPermissionAllowOverridesRoleDeny() =\n        let ownerId = Guid.NewGuid()\n        let organizationId = Guid.NewGuid()\n        let repositoryId = Guid.NewGuid()\n\n        let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-3\" }\n\n        let assignments =\n            [\n                createAssignment principal (Scope.Organization(ownerId, organizationId)) \"OrgReader\"\n            ]\n\n        let allowPermissions = List<ClaimPermission>()\n        allowPermissions.Add({ Claim = \"engineering\"; DirectoryPermission = DirectoryPermission.Modify })\n\n        let pathPermissions =\n            [\n                { Path = \"/images\"; Permissions = allowPermissions }\n            ]\n\n        let result =\n            checkPermission\n                roleCatalog\n                assignments\n                pathPermissions\n                [ principal ]\n                (Set.ofList [ \"engineering\" ])\n                Operation.PathWrite\n                (Resource.Path(ownerId, organizationId, repositoryId, \"/images\"))\n\n        assertAllowed result\n\n    [<Test>]\n    member _.NoPermissionsDenied() =\n        let ownerId = Guid.NewGuid()\n        let organizationId = Guid.NewGuid()\n        let repositoryId = Guid.NewGuid()\n\n        let principal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-4\" }\n\n        let result = checkPermission roleCatalog [] [] [ principal ] Set.empty Operation.RepoRead (Resource.Repository(ownerId, organizationId, repositoryId))\n\n        assertDenied result\n\n    [<Test>]\n    member _.GroupPrincipalAssignmentsApply() =\n        let ownerId = Guid.NewGuid()\n        let organizationId = Guid.NewGuid()\n        let repositoryId = Guid.NewGuid()\n\n        let userPrincipal = { PrincipalType = PrincipalType.User; PrincipalId = \"user-5\" }\n        let groupPrincipal = { PrincipalType = PrincipalType.Group; PrincipalId = \"group-1\" }\n\n        let assignments =\n            [\n                createAssignment groupPrincipal (Scope.Repository(ownerId, organizationId, repositoryId)) \"RepoReader\"\n            ]\n\n        let result =\n            checkPermission\n                roleCatalog\n                assignments\n                []\n                [ userPrincipal; groupPrincipal ]\n                Set.empty\n                Operation.RepoRead\n                (Resource.Repository(ownerId, organizationId, repositoryId))\n\n        assertAllowed result\n\n    [<Test>]\n    member _.RepoAdminIncludesBranchAdmin() =\n        let repoAdmin =\n            roleCatalog\n            |> List.find (fun role -> role.RoleId.Equals(\"RepoAdmin\", StringComparison.OrdinalIgnoreCase))\n\n        Assert.That(repoAdmin.AllowedOperations.Contains Operation.BranchAdmin, Is.True)\n"
  },
  {
    "path": "src/Grace.Server.Tests/Eventing.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server\nopen Grace.Shared.Utilities\nopen Grace.Types.Automation\nopen Grace.Types.Events\nopen Grace.Types.PromotionSet\nopen Grace.Types.Queue\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen Grace.Types.WorkItem\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\nopen System.Text.Json\n\n[<Parallelizable(ParallelScope.All)>]\ntype AutomationEventingTests() =\n\n    let metadata correlationId repositoryId =\n        let properties = Dictionary<string, string>()\n        properties[nameof RepositoryId] <- $\"{repositoryId}\"\n\n        { Timestamp = Instant.FromUtc(2026, 2, 18, 0, 0); CorrelationId = correlationId; Principal = \"tester\"; Properties = properties }\n\n    [<Test>]\n    member _.ReferencePromotionWithTerminalLinkMapsToPromotionSetApplied() =\n        let repositoryId = Guid.NewGuid()\n        let referenceId = Guid.NewGuid()\n        let promotionSetId = Guid.NewGuid()\n        let branchId = Guid.NewGuid()\n\n        let referenceEvent: ReferenceEvent =\n            {\n                Event =\n                    ReferenceEventType.Created(\n                        referenceId,\n                        Guid.NewGuid(),\n                        Guid.NewGuid(),\n                        repositoryId,\n                        branchId,\n                        Guid.NewGuid(),\n                        Sha256Hash String.Empty,\n                        ReferenceType.Promotion,\n                        \"promotion\",\n                        [\n                            ReferenceLinkType.IncludedInPromotionSet promotionSetId\n                            ReferenceLinkType.PromotionSetTerminal promotionSetId\n                        ]\n                    )\n                Metadata = metadata \"corr-terminal\" repositoryId\n            }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.ReferenceEvent referenceEvent)\n        Assert.That(envelope.IsSome, Is.True)\n\n        let eventEnvelope = envelope.Value\n        Assert.That(eventEnvelope.EventType, Is.EqualTo(AutomationEventType.PromotionSetApplied))\n        Assert.That(eventEnvelope.RepositoryId, Is.EqualTo(repositoryId))\n\n        use payload = JsonDocument.Parse(eventEnvelope.DataJson)\n\n        let payloadPromotionSetId =\n            payload\n                .RootElement\n                .GetProperty(\"promotionSetId\")\n                .GetGuid()\n\n        let payloadBranchId =\n            payload\n                .RootElement\n                .GetProperty(\"targetBranchId\")\n                .GetGuid()\n\n        let payloadReferenceId =\n            payload\n                .RootElement\n                .GetProperty(\"terminalPromotionReferenceId\")\n                .GetGuid()\n\n        Assert.That(payloadPromotionSetId, Is.EqualTo(promotionSetId))\n        Assert.That(payloadBranchId, Is.EqualTo(branchId))\n        Assert.That(payloadReferenceId, Is.EqualTo(referenceId))\n\n    [<Test>]\n    member _.ReferencePromotionWithoutTerminalLinkDoesNotEmitPromotionSetApplied() =\n        let repositoryId = Guid.NewGuid()\n\n        let referenceEvent: ReferenceEvent =\n            {\n                Event =\n                    ReferenceEventType.Created(\n                        Guid.NewGuid(),\n                        Guid.NewGuid(),\n                        Guid.NewGuid(),\n                        repositoryId,\n                        Guid.NewGuid(),\n                        Guid.NewGuid(),\n                        Sha256Hash String.Empty,\n                        ReferenceType.Promotion,\n                        \"promotion\",\n                        [\n                            ReferenceLinkType.IncludedInPromotionSet(Guid.NewGuid())\n                        ]\n                    )\n                Metadata = metadata \"corr-non-terminal\" repositoryId\n            }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.ReferenceEvent referenceEvent)\n        Assert.That(envelope.IsNone, Is.True)\n\n    [<Test>]\n    member _.QueuePromotionSetEnqueuedMapsToPromotionSetEnqueued() =\n        let repositoryId = Guid.NewGuid()\n\n        let queueEvent: PromotionQueueEvent =\n            { Event = PromotionQueueEventType.PromotionSetEnqueued(Guid.NewGuid()); Metadata = metadata \"corr-queue\" repositoryId }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.QueueEvent queueEvent)\n        Assert.That(envelope.IsSome, Is.True)\n        Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.PromotionSetEnqueued))\n        Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))\n\n    [<Test>]\n    member _.QueuePromotionSetDequeuedMapsToPromotionSetDequeued() =\n        let repositoryId = Guid.NewGuid()\n\n        let queueEvent: PromotionQueueEvent =\n            { Event = PromotionQueueEventType.PromotionSetDequeued(Guid.NewGuid()); Metadata = metadata \"corr-queue-dequeued\" repositoryId }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.QueueEvent queueEvent)\n        Assert.That(envelope.IsSome, Is.True)\n        Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.PromotionSetDequeued))\n        Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))\n\n    [<Test>]\n    member _.PromotionSetRecomputeStartedMapsToAutomationEvent() =\n        let repositoryId = Guid.NewGuid()\n        let metadata = metadata \"corr-ps-recompute-started\" repositoryId\n        metadata.Properties[ \"ActorId\" ] <- $\"{Guid.NewGuid()}\"\n\n        let promotionSetEvent: PromotionSetEvent = { Event = PromotionSetEventType.RecomputeStarted(Guid.NewGuid()); Metadata = metadata }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.PromotionSetEvent promotionSetEvent)\n        Assert.That(envelope.IsSome, Is.True)\n        Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.PromotionSetRecomputeStarted))\n        Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))\n\n    [<Test>]\n    member _.PromotionSetStepsUpdatedMapsToAutomationEvent() =\n        let repositoryId = Guid.NewGuid()\n        let metadata = metadata \"corr-ps-steps-updated\" repositoryId\n        metadata.Properties[ \"ActorId\" ] <- $\"{Guid.NewGuid()}\"\n\n        let step: PromotionSetStep =\n            {\n                StepId = Guid.NewGuid()\n                Order = 0\n                OriginalPromotion = { BranchId = Guid.NewGuid(); ReferenceId = Guid.NewGuid(); DirectoryVersionId = Guid.NewGuid() }\n                OriginalBasePromotionReferenceId = Guid.NewGuid()\n                OriginalBaseDirectoryVersionId = Guid.NewGuid()\n                ComputedAgainstBaseDirectoryVersionId = Guid.NewGuid()\n                AppliedDirectoryVersionId = Guid.NewGuid()\n                ConflictSummaryArtifactId = Option.None\n                ConflictStatus = StepConflictStatus.NoConflicts\n            }\n\n        let promotionSetEvent: PromotionSetEvent = { Event = PromotionSetEventType.StepsUpdated([ step ], Guid.NewGuid()); Metadata = metadata }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.PromotionSetEvent promotionSetEvent)\n        Assert.That(envelope.IsSome, Is.True)\n        Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.PromotionSetStepsUpdated))\n        Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))\n\n    [<Test>]\n    member _.WorkItemArtifactLinkedMapsToAgentSummaryAdded() =\n        let repositoryId = Guid.NewGuid()\n\n        let workItemEvent: WorkItemEvent =\n            {\n                Event = WorkItemEventType.ArtifactLinked(Guid.NewGuid())\n                Metadata = metadata \"corr-work-item-summary\" repositoryId\n            }\n\n        let envelope = EventingPublisher.tryCreateEnvelope (GraceEvent.WorkItemEvent workItemEvent)\n        Assert.That(envelope.IsSome, Is.True)\n        Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.AgentSummaryAdded))\n        Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))\n\n    [<Test>]\n    member _.AgentSessionEnvelopeRetainsCorrelationAndIdentityMetadata() =\n        let ownerId = Guid.NewGuid()\n        let organizationId = Guid.NewGuid()\n        let repositoryId = Guid.NewGuid()\n        let correlationId = \"corr-agent-session\"\n        let metadata = metadata correlationId repositoryId\n        metadata.Properties[nameof OwnerId] <- $\"{ownerId}\"\n        metadata.Properties[nameof OrganizationId] <- $\"{organizationId}\"\n        metadata.Properties[\"ActorId\"] <- \"agent-session\"\n\n        let operationResult =\n            {\n                AgentSessionOperationResult.Default with\n                    Session =\n                        {\n                            AgentSessionInfo.Default with\n                                SessionId = \"session-1\"\n                                AgentId = \"agent-123\"\n                                AgentDisplayName = \"Agent 123\"\n                                WorkItemIdOrNumber = \"42\"\n                                Source = \"cli\"\n                                LifecycleState = AgentSessionLifecycleState.Active\n                                StartedAt = Some(Instant.FromUtc(2026, 2, 18, 1, 0))\n                        }\n                    OperationId = \"op-1\"\n                    Message = \"started\"\n            }\n\n        let envelope =\n            EventingPublisher.tryCreateAgentSessionEnvelope\n                AutomationEventType.AgentBootstrapped\n                metadata\n                operationResult\n\n        Assert.That(envelope.IsSome, Is.True)\n        Assert.That(envelope.Value.EventType, Is.EqualTo(AutomationEventType.AgentBootstrapped))\n        Assert.That(envelope.Value.CorrelationId, Is.EqualTo(correlationId))\n        Assert.That(envelope.Value.OwnerId, Is.EqualTo(ownerId))\n        Assert.That(envelope.Value.OrganizationId, Is.EqualTo(organizationId))\n        Assert.That(envelope.Value.RepositoryId, Is.EqualTo(repositoryId))\n        Assert.That(envelope.Value.ActorId, Is.EqualTo(operationResult.Session.AgentId))\n\n        use payload = JsonDocument.Parse(envelope.Value.DataJson)\n        let payloadSessionId = payload.RootElement.GetProperty(\"Session\").GetProperty(\"SessionId\").GetString()\n        Assert.That(payloadSessionId, Is.EqualTo(operationResult.Session.SessionId))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Evidence.Determinism.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen DiffPlex.DiffBuilder.Model\nopen Grace.Shared\nopen Grace.Types.Diff\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen NUnit.Framework\nopen NodaTime\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype EvidenceDeterminism() =\n    let instant = Instant.FromUtc(2025, 1, 1, 0, 0)\n\n    let buildSection (line: string) (position: int) =\n        [|\n            DiffPiece(line, ChangeType.Modified, Nullable<int>(position))\n        |]\n\n    let buildSectionFromLines (lines: string list) =\n        lines\n        |> List.mapi (fun index line -> DiffPiece(line, ChangeType.Modified, Nullable<int>(index + 1)))\n        |> List.toArray\n\n\n    let buildFileDiff (relativePath: string) (lines: (string * int) list) =\n        let inlineDiff = List<DiffPiece []>()\n\n        for (text, position) in lines do\n            inlineDiff.Add(buildSection text position)\n\n        FileDiff.Create relativePath (Sha256Hash \"sha1\") instant (Sha256Hash \"sha2\") instant false inlineDiff (List<DiffPiece []>()) (List<DiffPiece []>())\n\n    let buildDiff (fileDiffs: FileDiff list) = { DiffDto.Default with HasDifferences = true; FileDiffs = List<FileDiff>(fileDiffs) }\n\n    [<Test>]\n    member _.EvidenceRedactionFlagsWhenPatternMatches() =\n        let file = buildFileDiff \"secrets.txt\" [ \"password=secret\", 1 ]\n        let budget = { MaxFiles = 5; MaxHunksPerFile = 5; MaxLinesPerHunk = 5; MaxTotalBytes = 4096; MaxTokens = 2000 }\n        let diff = buildDiff [ file ]\n\n        let evidence, _ = Evidence.buildEvidenceSet EvidenceStage.Triage budget None [ \"password=\\w+\" ] diff\n        let slice = evidence.Slices |> List.head\n\n        Assert.That(slice.IsRedacted, Is.True)\n        Assert.That(slice.Content, Does.Contain(\"***REDACTED***\"))\n\n    [<Test>]\n    member _.EvidenceBudgetsRespectHunksAndLines() =\n        let sections = List<DiffPiece []>()\n\n        sections.Add(\n            buildSectionFromLines [ \"one\"\n                                    \"two\"\n                                    \"three\" ]\n        )\n\n        sections.Add(buildSectionFromLines [ \"four\" ])\n\n        let inlineDiff = List<DiffPiece []>(sections)\n\n        let fileDiff =\n            FileDiff.Create \"multi.txt\" (Sha256Hash \"sha1\") instant (Sha256Hash \"sha2\") instant false inlineDiff (List<DiffPiece []>()) (List<DiffPiece []>())\n\n        let budget = { MaxFiles = 5; MaxHunksPerFile = 1; MaxLinesPerHunk = 1; MaxTotalBytes = 4096; MaxTokens = 2000 }\n        let diff = buildDiff [ fileDiff ]\n\n        let evidence, _ = Evidence.buildEvidenceSet EvidenceStage.Triage budget None [] diff\n        let slice = evidence.Slices |> List.head\n\n        Assert.That(evidence.Slices.Length, Is.EqualTo(1))\n        Assert.That(slice.StartLine, Is.EqualTo(1))\n        Assert.That(slice.EndLine, Is.EqualTo(1))\n        Assert.That(slice.Content, Is.EqualTo(\"one\"))\n\n    [<Test>]\n    member _.EvidenceBudgetCapsTotalBytes() =\n        let file = buildFileDiff \"tiny.txt\" [ \"a\", 1; \"b\", 2 ]\n        let budget = { MaxFiles = 5; MaxHunksPerFile = 5; MaxLinesPerHunk = 5; MaxTotalBytes = 1; MaxTokens = 2000 }\n        let diff = buildDiff [ file ]\n\n        let evidence, _ = Evidence.buildEvidenceSet EvidenceStage.Triage budget None [] diff\n        Assert.That(evidence.Slices.Length, Is.EqualTo(1))\n\n    [<Test>]\n    member _.EvidenceSelectionIsDeterministicAcrossFileOrdering() =\n        let fileA = buildFileDiff \"a.txt\" [ \"alpha\", 1; \"beta\", 2 ]\n        let fileB = buildFileDiff \"b.txt\" [ \"gamma\", 1 ]\n\n        let budget = { MaxFiles = 5; MaxHunksPerFile = 5; MaxLinesPerHunk = 5; MaxTotalBytes = 4096; MaxTokens = 2000 }\n\n        let diffOne = buildDiff [ fileB; fileA ]\n        let diffTwo = buildDiff [ fileA; fileB ]\n\n        let evidenceOne, summaryOne = Evidence.buildEvidenceSet EvidenceStage.Triage budget None [] diffOne\n        let evidenceTwo, summaryTwo = Evidence.buildEvidenceSet EvidenceStage.Triage budget None [] diffTwo\n\n        let selectedFilesMatch = summaryOne.SelectedFiles = summaryTwo.SelectedFiles\n        let summariesMatch = summaryOne.SliceSummaries = summaryTwo.SliceSummaries\n        let slicesMatch = evidenceOne.Slices = evidenceTwo.Slices\n\n        Assert.That(selectedFilesMatch, Is.True)\n        Assert.That(summariesMatch, Is.True)\n        Assert.That(slicesMatch, Is.True)\n\n    [<Test>]\n    member _.EvidenceSelectionRespectsSortedBudgetOrdering() =\n        let fileA = buildFileDiff \"a.txt\" [ \"alpha\", 1 ]\n        let fileB = buildFileDiff \"b.txt\" [ \"gamma\", 1 ]\n\n        let budget = { MaxFiles = 1; MaxHunksPerFile = 5; MaxLinesPerHunk = 5; MaxTotalBytes = 4096; MaxTokens = 2000 }\n\n        let diff = buildDiff [ fileB; fileA ]\n        let _, summary = Evidence.buildEvidenceSet EvidenceStage.Triage budget None [] diff\n\n        let selected = summary.SelectedFiles = [ \"a.txt\" ]\n        Assert.That(selected, Is.True)\n"
  },
  {
    "path": "src/Grace.Server.Tests/General.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Json\nopen System.Text.Json\nopen System.Threading\nopen System.Threading.Tasks\nopen Grace.Types.Types\nopen Grace.Types\nopen FSharp.Control\nopen Grace.Shared.Validation.Errors\n\nmodule Common =\n    let okResult: Result<unit, TestError> = Result.Ok()\n    let errorResult: Result<unit, TestError> = Result.Error TestError.TestFailed\n\n    let tryGetGuidProperty (value: obj) =\n        let mutable parsed = Guid.Empty\n\n        match value with\n        | :? Guid as guid -> Some guid\n        | :? string as text when Guid.TryParse(text, &parsed) -> Some parsed\n        | :? JsonElement as element ->\n            if element.ValueKind = JsonValueKind.String then\n                let text = element.GetString()\n                if Guid.TryParse(text, &parsed) then Some parsed else None\n            else\n                None\n        | _ -> None\n\n    let requireGuidProperty (name: string) (value: obj) =\n        match tryGetGuidProperty value with\n        | Some guid -> guid\n        | None -> failwith $\"Property '{name}' was not a GUID value.\"\n\nmodule Services =\n    [<Literal>]\n    let numberOfRepositories = 3\n\n    let rnd = Random.Shared\n\n    let mutable App: Aspire.Hosting.DistributedApplication option = None\n    let mutable Client: HttpClient = Unchecked.defaultof<HttpClient>\n\n    let mutable ownerId = String.Empty\n    let mutable organizationId = String.Empty\n    let mutable repositoryIds: string [] = Array.empty\n    let mutable repositoryDefaultBranchIds: string [] = Array.empty\n\n    let mutable serviceBusConnectionString = String.Empty\n    let mutable serviceBusTopic = String.Empty\n    let mutable serviceBusServerSubscription = String.Empty\n    let mutable serviceBusTestSubscription = String.Empty\n    let mutable graceServerBaseAddress = String.Empty\n    let mutable testUserId = String.Empty\n    let mutable testUserClaims: string list = []\n\n    let logToTestConsole (message: string) = Console.WriteLine(message)\n\nopen Services\n\n/// Defines the setup and teardown for all tests in the Grace.Server.Tests namespace.\n[<SetUpFixture>]\ntype Setup() =\n\n    [<OneTimeSetUp>]\n    member public _.Setup() =\n        task {\n            logToTestConsole \"Starting Aspire test host...\"\n\n            testUserId <- $\"{Guid.NewGuid()}\"\n            let! hostState = AspireTestHost.startAsync testUserId\n\n            App <- Some hostState.App\n            Client <- hostState.Client\n            graceServerBaseAddress <- hostState.GraceServerBaseAddress\n            serviceBusConnectionString <- hostState.ServiceBusConnectionString\n            serviceBusTopic <- hostState.ServiceBusTopic\n            serviceBusServerSubscription <- hostState.ServiceBusServerSubscription\n            serviceBusTestSubscription <- hostState.ServiceBusTestSubscription\n            testUserClaims <- [ \"engineering\"; \"contributors\" ]\n\n            Client.DefaultRequestHeaders.Add(\"x-grace-user-id\", testUserId)\n\n            logToTestConsole $\"Grace.Server base address: {graceServerBaseAddress}\"\n\n            logToTestConsole\n                $\"Service Bus topic: {serviceBusTopic}; subscription: {serviceBusServerSubscription}; test subscription: {serviceBusTestSubscription}\"\n\n            let! drained = AspireTestHost.drainServiceBusAsync hostState\n\n            if drained > 0 then\n                logToTestConsole $\"Drained {drained} message(s) from Service Bus test subscription before tests.\"\n\n            let correlationId = generateCorrelationId ()\n\n            ownerId <- $\"{Guid.NewGuid()}\"\n            let ownerName = $\"TestOwner{rnd.Next(65535):X4}\"\n\n            let ownerParameters = Parameters.Owner.CreateOwnerParameters()\n            ownerParameters.OwnerId <- ownerId\n            ownerParameters.OwnerName <- ownerName\n            ownerParameters.CorrelationId <- correlationId\n\n            let! ownerResponse = Client.PostAsync(\"/owner/create\", createJsonContent ownerParameters)\n\n            if ownerResponse.IsSuccessStatusCode then\n                logToTestConsole $\"Owner {ownerParameters.OwnerName} created successfully.\"\n            else\n                let! content = ownerResponse.Content.ReadAsStringAsync()\n                Assert.That(content.Length, Is.GreaterThan(0))\n                let error = deserialize<GraceError> content\n                logToTestConsole $\"StatusCode: {ownerResponse.StatusCode}; Content: {error}\"\n\n            ownerResponse.EnsureSuccessStatusCode() |> ignore\n\n            let getOwnerParameters = Parameters.Owner.GetOwnerParameters()\n            getOwnerParameters.OwnerId <- ownerId\n            getOwnerParameters.CorrelationId <- correlationId\n\n            let! getOwnerResponse = Client.PostAsync(\"/owner/get\", createJsonContent getOwnerParameters)\n\n            getOwnerResponse.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! getOwnerReturnValue = deserializeContent<GraceReturnValue<Owner.OwnerDto>> getOwnerResponse\n            Assert.That(getOwnerReturnValue.ReturnValue.OwnerId, Is.EqualTo(Guid.Parse(ownerId)))\n            Assert.That(getOwnerReturnValue.ReturnValue.OwnerName, Is.EqualTo(ownerName))\n\n            let! _ = AspireTestHost.waitForOwnerCreatedEventAsync hostState ownerId\n            logToTestConsole $\"Owner Created event observed on Service Bus test subscription for {ownerId}.\"\n\n            organizationId <- $\"{Guid.NewGuid()}\"\n            repositoryIds <- Array.init numberOfRepositories (fun _ -> $\"{Guid.NewGuid()}\")\n            repositoryDefaultBranchIds <- Array.zeroCreate numberOfRepositories\n\n            let organizationParameters = Parameters.Organization.CreateOrganizationParameters()\n            organizationParameters.OwnerId <- ownerId\n            organizationParameters.OrganizationId <- organizationId\n            organizationParameters.OrganizationName <- $\"TestOrganization{rnd.Next(65535):X4}\"\n            organizationParameters.CorrelationId <- correlationId\n\n            let! organizationResponse = Client.PostAsync(\"/organization/create\", createJsonContent organizationParameters)\n\n            if organizationResponse.IsSuccessStatusCode then\n                logToTestConsole $\"Organization {organizationParameters.OrganizationName} created successfully.\"\n            else\n                let! content = organizationResponse.Content.ReadAsStringAsync()\n                Assert.That(content.Length, Is.GreaterThan(0))\n                let error = deserialize<GraceError> content\n                logToTestConsole $\"StatusCode: {organizationResponse.StatusCode}; Content: {error}\"\n\n            organizationResponse.EnsureSuccessStatusCode()\n            |> ignore\n\n            do!\n                Parallel.ForEachAsync(\n                    Array.indexed repositoryIds,\n                    Constants.ParallelOptions,\n                    (fun repositoryInfo ct ->\n                        let repositoryIndex, repositoryId = repositoryInfo\n\n                        ValueTask(\n                            task {\n                                let repositoryParameters = Parameters.Repository.CreateRepositoryParameters()\n                                repositoryParameters.OwnerId <- ownerId\n                                repositoryParameters.OrganizationId <- organizationId\n                                repositoryParameters.RepositoryId <- repositoryId\n                                repositoryParameters.RepositoryName <- $\"TestRepository{rnd.Next():X8}\"\n                                repositoryParameters.CorrelationId <- correlationId\n\n                                let! response = Client.PostAsync(\"/repository/create\", createJsonContent repositoryParameters)\n\n                                if response.IsSuccessStatusCode then\n                                    logToTestConsole $\"Repository {repositoryParameters.RepositoryName} created successfully.\"\n                                else\n                                    let! content = response.Content.ReadAsStringAsync()\n                                    Assert.That(content.Length, Is.GreaterThan(0))\n                                    let error = deserialize<GraceError> content\n                                    logToTestConsole $\"StatusCode: {response.StatusCode}; Content: {error}\"\n\n                                response.EnsureSuccessStatusCode() |> ignore\n\n                                let! returnValue = deserializeContent<GraceReturnValue<string>> response\n                                let branchId = Common.requireGuidProperty (nameof BranchId) returnValue.Properties[nameof BranchId]\n                                repositoryDefaultBranchIds[repositoryIndex] <- $\"{branchId}\"\n                            }\n                        ))\n                )\n        }\n\n    [<OneTimeTearDown>]\n    member public _.Teardown() =\n        task {\n            let correlationId = generateCorrelationId ()\n            let logCleanupFailure (label: string) (detail: string) = logToTestConsole $\"Cleanup {label} failed: {detail}\"\n\n            let cleanupEnabled =\n                match Environment.GetEnvironmentVariable(\"GRACE_TEST_CLEANUP\") with\n                | null -> false\n                | value ->\n                    value.Equals(\"1\", StringComparison.OrdinalIgnoreCase)\n                    || value.Equals(\"true\", StringComparison.OrdinalIgnoreCase)\n                    || value.Equals(\"yes\", StringComparison.OrdinalIgnoreCase)\n\n            let tryPost (label: string) (path: string) (content: HttpContent) =\n                task {\n                    try\n                        use cts = new CancellationTokenSource(TimeSpan.FromSeconds(15.0))\n                        let! response = Client.PostAsync(path, content, cts.Token)\n\n                        if response.IsSuccessStatusCode then\n                            ()\n                        else\n                            let! body = response.Content.ReadAsStringAsync()\n                            logCleanupFailure label $\"StatusCode: {response.StatusCode}; Content: {body}\"\n                    with\n                    | ex -> logCleanupFailure label ex.Message\n                }\n\n            if cleanupEnabled then\n                try\n                    if not <| String.IsNullOrWhiteSpace ownerId then\n                        if repositoryIds.Length > 0 then\n                            for repositoryId in repositoryIds do\n                                let repositoryDeleteParameters = Parameters.Repository.DeleteRepositoryParameters()\n\n                                repositoryDeleteParameters.OwnerId <- ownerId\n                                repositoryDeleteParameters.OrganizationId <- organizationId\n                                repositoryDeleteParameters.RepositoryId <- repositoryId\n                                repositoryDeleteParameters.DeleteReason <- \"Deleting test repository\"\n                                repositoryDeleteParameters.CorrelationId <- correlationId\n                                repositoryDeleteParameters.Force <- true\n\n                                do! tryPost $\"repository {repositoryId}\" \"/repository/delete\" (createJsonContent repositoryDeleteParameters)\n\n                        if not <| String.IsNullOrWhiteSpace organizationId then\n                            let organizationDeleteParameters = Parameters.Organization.DeleteOrganizationParameters()\n\n                            organizationDeleteParameters.OwnerId <- ownerId\n                            organizationDeleteParameters.OrganizationId <- organizationId\n                            organizationDeleteParameters.DeleteReason <- \"Deleting test organization\"\n                            organizationDeleteParameters.CorrelationId <- correlationId\n                            organizationDeleteParameters.Force <- true\n\n                            do! tryPost $\"organization {organizationId}\" \"/organization/delete\" (createJsonContent organizationDeleteParameters)\n\n                        let ownerDeleteParameters = Parameters.Owner.DeleteOwnerParameters()\n                        ownerDeleteParameters.OwnerId <- ownerId\n                        ownerDeleteParameters.DeleteReason <- \"Deleting test owner\"\n                        ownerDeleteParameters.CorrelationId <- generateCorrelationId ()\n                        ownerDeleteParameters.Force <- true\n\n                        do! tryPost $\"owner {ownerId}\" \"/owner/delete\" (createJsonContent ownerDeleteParameters)\n                with\n                | ex -> logCleanupFailure \"cleanup\" ex.Message\n            else\n                logToTestConsole \"Skipping server-side cleanup (set GRACE_TEST_CLEANUP=1 to enable).\"\n\n            if not (isNull Client) then Client.Dispose()\n\n            try\n                do! AspireTestHost.stopAsync App\n            with\n            | ex -> logCleanupFailure \"apphost stop\" ex.Message\n\n        }\n\n[<Parallelizable(ParallelScope.All)>]\ntype General() =\n\n    [<Test>]\n    member public _.RootPathReturnsValue() =\n        task {\n            let! response = Services.Client.GetAsync(\"/\")\n            let! content = response.Content.ReadAsStringAsync()\n            Console.WriteLine($\"{content}\")\n            Assert.That(content, Does.Contain(\"Grace\"))\n        }\n\n    [<Test>]\n    member public _.MetricsRequiresAuthentication() =\n        task {\n            use client = new HttpClient()\n            client.BaseAddress <- Services.Client.BaseAddress\n\n            let! response = client.GetAsync(\"/metrics\")\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized))\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/Grace.Server.Tests.fsproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <PublishReadyToRun>false</PublishReadyToRun>\n    <LangVersion>preview</LangVersion>\n    <IsPackable>false</IsPackable>\n    <IsAspireHost>false</IsAspireHost>\n    <IsTestProject>true</IsTestProject>\n    <GenerateProgramFile>false</GenerateProgramFile>\n    <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    <OtherFlags>--test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen</OtherFlags>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"AspireTestHost.fs\" />\n    <Compile Include=\"General.Server.Tests.fs\" />\n    <Compile Include=\"Smoke.Server.Tests.fs\" />\n    <Compile Include=\"Slop.Server.Tests.fs\" />\n    <Compile Include=\"Validations.Server.Tests.fs\" />\n    <Compile Include=\"Owner.Server.Tests.fs\" />\n    <Compile Include=\"Repository.Server.Tests.fs\" />\n    <Compile Include=\"AuthMapping.Unit.Tests.fs\" />\n    <Compile Include=\"Authorization.Unit.Tests.fs\" />\n    <Compile Include=\"Access.Server.Tests.fs\" />\n    <Compile Include=\"Eventing.Server.Tests.fs\" />\n    <Compile Include=\"Notification.Server.Tests.fs\" />\n    <Compile Include=\"PromotionSet.CommandValidation.Tests.fs\" />\n    <Compile Include=\"Services.EffectivePromotion.Tests.fs\" />\n    <Compile Include=\"Validation.Artifact.Contract.Tests.fs\" />\n    <Compile Include=\"Policy.Determinism.Tests.fs\" />\n    <Compile Include=\"Policy.Validation.Derived.Tests.fs\" />\n    <Compile Include=\"Evidence.Determinism.Tests.fs\" />\n    <Compile Include=\"ReviewNotes.Determinism.Tests.fs\" />\n    <Compile Include=\"Review.Server.Tests.fs\" />\n    <Compile Include=\"Queue.Server.Tests.fs\" />\n    <Compile Include=\"WorkItem.Server.Tests.fs\" />\n    <Compile Include=\"OrleansFilters.Server.Tests.fs\" />\n    <Compile Include=\"WorkItem.Integration.Server.Tests.fs\" />\n    <Compile Include=\"Auth.Server.Tests.fs\" />\n    <Compile Include=\"Program.fs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n    <PackageReference Include=\"Aspire.Hosting.Testing\" Version=\"13.1.0\" />\n    <PackageReference Include=\"FSharp.Control.TaskSeq\" Version=\"0.4.0\" />\n    <PackageReference Include=\"FsCheck\" Version=\"3.2.0\" />\n    <PackageReference Include=\"FsCheck.NUnit\" Version=\"3.2.0\" />\n    <PackageReference Include=\"FsUnit\" Version=\"7.1.1\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n    <PackageReference Include=\"NUnit\" Version=\"4.4.0\" />\n    <PackageReference Include=\"NUnit3TestAdapter\" Version=\"5.2.0\" />\n    <PackageReference Include=\"NUnit.Analyzers\" Version=\"4.11.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Grace.Aspire.AppHost\\Grace.Aspire.AppHost.csproj\" />\n    <ProjectReference Include=\"..\\Grace.SDK\\Grace.SDK.fsproj\" />\n    <ProjectReference Include=\"..\\Grace.Server\\Grace.Server.fsproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Server.Tests/Notification.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server.Notification\nopen Grace.Types.Types\nopen NUnit.Framework\n\n[<Parallelizable(ParallelScope.All)>]\ntype NotificationServerTests() =\n\n    [<TestCase(\"main\", \"main\", true)>]\n    [<TestCase(\"MAIN\", \"main\", true)>]\n    [<TestCase(\"release/2026.02\", \"release/*\", true)>]\n    [<TestCase(\"feature/promo-set\", \"feature/*\", true)>]\n    [<TestCase(\"main\", \"*\", true)>]\n    [<TestCase(\"release\", \"main\", false)>]\n    [<TestCase(\"feature/promo\", \"release/*\", false)>]\n    member _.BranchNameGlobMatchingIsCaseInsensitiveAndSupportsWildcard(branchName: string, glob: string, expected: bool) =\n        let actual = Subscriber.matchesBranchGlob (BranchName branchName) glob\n        Assert.That(actual, Is.EqualTo(expected))\n"
  },
  {
    "path": "src/Grace.Server.Tests/OrleansFilters.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen NUnit.Framework\nopen System\nopen System.IO\n\n[<Parallelizable(ParallelScope.All)>]\ntype OrleansPartitionKeyProviderTests() =\n\n    let tryResolveSourcePath () =\n        let mutable current = DirectoryInfo(Environment.CurrentDirectory)\n        let mutable resolvedPath = String.Empty\n\n        while\n            isNull current |> not\n            && String.IsNullOrWhiteSpace(resolvedPath)\n            do\n            let candidate = Path.Combine(current.FullName, \"src\", \"Grace.Server\", \"OrleansFilters.Server.fs\")\n\n            if File.Exists(candidate) then\n                resolvedPath <- candidate\n            else\n                current <- current.Parent\n\n        if String.IsNullOrWhiteSpace(resolvedPath) then\n            failwith \"Could not locate src/Grace.Server/OrleansFilters.Server.fs from the current test directory.\"\n        else\n            resolvedPath\n\n    [<Test>]\n    member _.WorkItemNumberCounterMapsToRepositoryPartitionKey() =\n        let filePath = tryResolveSourcePath ()\n        let sourceText = File.ReadAllText(filePath)\n\n        Assert.That(\n            sourceText,\n            Does.Contain(\"| StateName.WorkItemNumberCounter -> repositoryId ()\")\n        )\n"
  },
  {
    "path": "src/Grace.Server.Tests/Owner.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen FSharp.Control\nopen FSharpPlus\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen NUnit.Framework\nopen System\nopen System.Net.Http.Json\nopen System.Net\nopen System.Threading.Tasks\nopen System.IO\nopen System.Text\nopen System.Diagnostics\nopen Grace.Types.Types\nopen System.Net.Http\n\n[<Parallelizable(ParallelScope.All)>]\ntype Owner() =\n\n    let log =\n        LoggerFactory\n            .Create(fun builder -> builder.AddConsole().AddDebug() |> ignore)\n            .CreateLogger(\"Owner.Server.Tests\")\n\n    member val public TestContext = TestContext.CurrentContext with get, set\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithValidValues() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerDescriptionParameters()\n\n            parameters.Description <- $\"Description set at {getCurrentInstantGeneral ()}.\"\n            parameters.OwnerId <- ownerId\n\n            let! response = Client.PostAsync(\"/owner/setDescription\", createJsonContent parameters)\n            let! content = response.Content.ReadAsStringAsync()\n            response.EnsureSuccessStatusCode() |> ignore\n            Assert.That(content.Length, Is.GreaterThan(0))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetTypeToPublic() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerTypeParameters()\n            parameters.OwnerType <- \"Public\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setType\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<string>> response\n            let ownerGuid = Common.requireGuidProperty (nameof OwnerId) returnValue.Properties[nameof OwnerId]\n            Assert.That(ownerGuid, Is.EqualTo(Guid.Parse(ownerId)))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetTypeToPrivate() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerTypeParameters()\n            parameters.OwnerType <- \"Private\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setType\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<string>> response\n            let ownerGuid = Common.requireGuidProperty (nameof OwnerId) returnValue.Properties[nameof OwnerId]\n            Assert.That(ownerGuid, Is.EqualTo(Guid.Parse(ownerId)))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetTypeToInvalidType() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerTypeParameters()\n            parameters.OwnerType <- \"Not a valid owner type\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setType\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.InvalidOwnerType))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetTypeToEmptyType() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerTypeParameters()\n            parameters.OwnerType <- String.Empty\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setType\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.OwnerTypeIsRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithInvalidDescription() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerDescriptionParameters()\n            parameters.Description <- \"a\".PadRight(2049, 'a')\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.DescriptionIsTooLong))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithInvalidOwnerId() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerDescriptionParameters()\n\n            parameters.Description <- $\"Description set at {getCurrentInstantGeneral ()}.\"\n            parameters.OwnerId <- \"this is not an owner id\"\n\n            let! response = Client.PostAsync(\"/owner/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.InvalidOwnerId))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithEmptyDescription() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerDescriptionParameters()\n\n            parameters.Description <- \"\"\n            parameters.OwnerId <- ownerId\n\n            let! response = Client.PostAsync(\"/owner/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.DescriptionIsRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithEmptyOwnerId() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerDescriptionParameters()\n            parameters.Description <- $\"Description set at {getCurrentInstantGeneral ()}.\"\n            parameters.OwnerId <- \"\"\n            let! response = Client.PostAsync(\"/owner/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.EitherOwnerIdOrOwnerNameRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSearchVisibilityToVisible() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerSearchVisibilityParameters()\n            parameters.SearchVisibility <- \"Visible\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setSearchVisibility\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<string>> response\n            let ownerGuid = Common.requireGuidProperty (nameof OwnerId) returnValue.Properties[nameof OwnerId]\n            Assert.That(ownerGuid, Is.EqualTo(Guid.Parse(ownerId)))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSearchVisibilityToNotVisible() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerSearchVisibilityParameters()\n            parameters.SearchVisibility <- \"NotVisible\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setSearchVisibility\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<string>> response\n            let ownerGuid = Common.requireGuidProperty (nameof OwnerId) returnValue.Properties[nameof OwnerId]\n            Assert.That(ownerGuid, Is.EqualTo(Guid.Parse(ownerId)))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSearchVisibilityToInvalidVisibility() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerSearchVisibilityParameters()\n            parameters.SearchVisibility <- \"Not a valid search visibility\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setSearchVisibility\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.InvalidSearchVisibility))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSearchVisibilityToEmptyVisibility() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerSearchVisibilityParameters()\n            parameters.SearchVisibility <- String.Empty\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setSearchVisibility\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.SearchVisibilityIsRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSearchVisibilityWithInvalidOwnerId() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerSearchVisibilityParameters()\n            parameters.SearchVisibility <- \"Visible\"\n            parameters.OwnerId <- \"this is not an owner id\"\n            let! response = Client.PostAsync(\"/owner/setSearchVisibility\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.InvalidOwnerId))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSearchVisibilityWithEmptyOwnerId() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerSearchVisibilityParameters()\n            parameters.SearchVisibility <- \"Visible\"\n            parameters.OwnerId <- \"\"\n            let! response = Client.PostAsync(\"/owner/setSearchVisibility\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.EitherOwnerIdOrOwnerNameRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetNameToValidName() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerNameParameters()\n            parameters.NewName <- $\"NewOwnerName{rnd.NextInt64()}\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setName\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<string>> response\n            let ownerGuid = Common.requireGuidProperty (nameof OwnerId) returnValue.Properties[nameof OwnerId]\n            Assert.That(ownerGuid, Is.EqualTo(Guid.Parse(ownerId)))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetNameToInvalidName() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerNameParameters()\n            parameters.NewName <- \"doesn't match Regex\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setName\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.InvalidOwnerName))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetNameToEmptyName() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerNameParameters()\n            parameters.NewName <- \"\"\n            parameters.OwnerId <- ownerId\n            let! response = Client.PostAsync(\"/owner/setName\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.OwnerNameIsRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetNameWithInvalidOwnerId() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerNameParameters()\n            parameters.NewName <- $\"NewOwnerName{rnd.NextInt64()}\"\n            parameters.OwnerId <- \"this is not an owner id\"\n            let! response = Client.PostAsync(\"/owner/setName\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.InvalidOwnerId))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetNameWithEmptyOwnerId() =\n        task {\n            let parameters = Parameters.Owner.SetOwnerNameParameters()\n            parameters.NewName <- $\"NewOwnerName{rnd.NextInt64()}\"\n            parameters.OwnerId <- \"\"\n            let! response = Client.PostAsync(\"/owner/setName\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage OwnerError.EitherOwnerIdOrOwnerNameRequired))\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/Policy.Determinism.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\nopen System.Security.Cryptography\nopen System.Text\n\n[<Parallelizable(ParallelScope.All)>]\ntype PolicyDeterminism() =\n    let extractPolicyBlock (markdown: string) =\n        let startMarker = \"```grace-policy\"\n        let startIndex = markdown.IndexOf(startMarker, StringComparison.OrdinalIgnoreCase)\n\n        if startIndex < 0 then\n            None\n        else\n            let afterStart = markdown.IndexOf('\\n', startIndex)\n\n            if afterStart < 0 then\n                None\n            else\n                let endIndex = markdown.IndexOf(\"```\", afterStart + 1, StringComparison.Ordinal)\n\n                if endIndex < 0 then\n                    None\n                else\n                    markdown\n                        .Substring(afterStart + 1, endIndex - afterStart - 1)\n                        .Trim()\n                    |> Some\n\n    let computeSnapshotId (parserVersion: string) (policyBlock: string) =\n        let bytes = Encoding.UTF8.GetBytes($\"{parserVersion}\\n{policyBlock}\")\n        let hashBytes = SHA256.HashData(bytes)\n        Sha256Hash(byteArrayToString (hashBytes.AsSpan()))\n\n    [<Test>]\n    member _.ExtractPolicyBlockReturnsBlockContents() =\n        let markdown =\n            String.concat\n                \"\\n\"\n                [\n                    \"# Grace Instructions\"\n                    \"Intro text.\"\n                    \"```grace-policy\"\n                    \"version: 1\"\n                    \"defaults: {}\"\n                    \"```\"\n                    \"More details.\"\n                ]\n\n        let result = extractPolicyBlock markdown\n        Assert.That(result, Is.EqualTo(Some(\"version: 1\\ndefaults: {}\")))\n\n    [<Test>]\n    member _.SnapshotIdStableForSameContent() =\n        let policyBlock = \"version: 1\\nqueue:\\n  onFailure: pause\"\n        let parserVersion = \"v1\"\n        let first = computeSnapshotId parserVersion policyBlock\n        let second = computeSnapshotId parserVersion policyBlock\n        Assert.That(first, Is.EqualTo(second))\n\n    [<Test>]\n    member _.SnapshotIdChangesWhenParserVersionChanges() =\n        let policyBlock = \"version: 1\\nqueue:\\n  onFailure: pause\"\n        let first = computeSnapshotId \"v1\" policyBlock\n        let second = computeSnapshotId \"v2\" policyBlock\n        Assert.That(first, Is.Not.EqualTo(second))\n\n    [<Test>]\n    member _.SnapshotIdChangesWhenPolicyChanges() =\n        let parserVersion = \"v1\"\n        let first = computeSnapshotId parserVersion \"version: 1\\nqueue:\\n  onFailure: pause\"\n        let second = computeSnapshotId parserVersion \"version: 1\\nqueue:\\n  onFailure: continue\"\n        Assert.That(first, Is.Not.EqualTo(second))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Policy.Validation.Derived.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server\nopen Grace.Shared.Parameters.Policy\nopen Grace.Shared.Validation.Errors\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen NUnit.Framework\nopen NodaTime\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype PolicyValidationDerivedTests() =\n    let metadata correlationId timestamp =\n        { Timestamp = timestamp; CorrelationId = correlationId; Principal = \"tester\"; Properties = Dictionary<string, string>() }\n\n    [<Test>]\n    member _.ValidationResultRejectsDuplicateCorrelationIds() =\n        let timestamp = Instant.FromUtc(2025, 3, 1, 0, 0)\n        let eventMetadata = metadata \"corr-validation\" timestamp\n\n        let validationResultEvent: ValidationResultEvent = { Event = ValidationResultEventType.Recorded ValidationResultDto.Default; Metadata = eventMetadata }\n\n        let duplicate = Grace.Actors.ValidationResult.hasDuplicateCorrelationId [ validationResultEvent ] eventMetadata\n\n        let different = Grace.Actors.ValidationResult.hasDuplicateCorrelationId [ validationResultEvent ] { eventMetadata with CorrelationId = \"corr-other\" }\n\n        Assert.That(duplicate, Is.True)\n        Assert.That(different, Is.False)\n\n    [<Test>]\n    member _.DerivedComputationQuickScanPredicateMatchesReferenceTypes() =\n        Assert.That(DerivedComputation.shouldRecordQuickScan ReferenceType.Commit, Is.True)\n        Assert.That(DerivedComputation.shouldRecordQuickScan ReferenceType.Checkpoint, Is.True)\n        Assert.That(DerivedComputation.shouldRecordQuickScan ReferenceType.Promotion, Is.True)\n        Assert.That(DerivedComputation.shouldRecordQuickScan ReferenceType.Save, Is.False)\n\n    [<Test>]\n    member _.PolicyAcknowledgeRejectsMissingSnapshotId() =\n        let parameters = AcknowledgePolicyParameters(TargetBranchId = Guid.NewGuid().ToString(), PolicySnapshotId = String.Empty)\n\n        let validations = Policy.validateAcknowledgeParameters parameters\n\n        let error =\n            validations\n            |> getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(error, Is.EqualTo(Some PolicyError.InvalidPolicySnapshotId))\n\n    [<Test>]\n    member _.PolicyAcknowledgeRejectsInvalidBranchId() =\n        let parameters = AcknowledgePolicyParameters(TargetBranchId = \"not-a-guid\", PolicySnapshotId = \"snapshot\")\n\n        let validations = Policy.validateAcknowledgeParameters parameters\n\n        let error =\n            validations\n            |> getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(error, Is.EqualTo(Some PolicyError.InvalidTargetBranchId))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Program.fs",
    "content": "namespace Grace.Server.Tests\n\nopen NUnit.Framework\n\nmodule Program =\n\n    [<EntryPoint>]\n    let main _ = 0\n"
  },
  {
    "path": "src/Grace.Server.Tests/PromotionSet.CommandValidation.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Actors\nopen Grace.Types.Events\nopen Grace.Types.PromotionSet\nopen Grace.Types.Types\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype PromotionSetCommandValidationTests() =\n\n    let createMetadata correlationId =\n        { Timestamp = Instant.FromUtc(2026, 2, 21, 11, 0); CorrelationId = correlationId; Principal = \"tester\"; Properties = Dictionary<string, string>() }\n\n    let existingPromotionSet status computationStatus =\n        { PromotionSetDto.Default with\n            PromotionSetId = Guid.NewGuid()\n            OwnerId = Guid.NewGuid()\n            OrganizationId = Guid.NewGuid()\n            RepositoryId = Guid.NewGuid()\n            TargetBranchId = Guid.NewGuid()\n            Status = status\n            StepsComputationStatus = computationStatus\n        }\n\n    [<Test>]\n    member _.ApplyRejectedWhenPromotionSetAlreadySucceeded() =\n        let dto = existingPromotionSet PromotionSetStatus.Succeeded StepsComputationStatus.Computed\n        let metadata = createMetadata \"corr-apply-succeeded\"\n\n        match PromotionSet.validateCommandForState [] dto PromotionSetCommand.Apply metadata with\n        | Ok _ -> Assert.Fail(\"Expected apply validation to fail for succeeded PromotionSet.\")\n        | Error graceError -> Assert.That(graceError.Error, Is.EqualTo(\"PromotionSet has already been applied successfully.\"))\n\n    [<Test>]\n    member _.ApplyRejectedWhenPromotionSetAlreadyRunning() =\n        let dto = existingPromotionSet PromotionSetStatus.Running StepsComputationStatus.Computing\n        let metadata = createMetadata \"corr-apply-running\"\n\n        match PromotionSet.validateCommandForState [] dto PromotionSetCommand.Apply metadata with\n        | Ok _ -> Assert.Fail(\"Expected apply validation to fail for running PromotionSet.\")\n        | Error graceError -> Assert.That(graceError.Error, Is.EqualTo(\"PromotionSet is already running.\"))\n\n    [<Test>]\n    member _.RecomputeRejectedWhenStepsAlreadyComputing() =\n        let dto = existingPromotionSet PromotionSetStatus.Ready StepsComputationStatus.Computing\n        let metadata = createMetadata \"corr-recompute-computing\"\n\n        match PromotionSet.validateCommandForState [] dto (PromotionSetCommand.RecomputeStepsIfStale(Option.None)) metadata with\n        | Ok _ -> Assert.Fail(\"Expected recompute validation to fail while already computing.\")\n        | Error graceError -> Assert.That(graceError.Error, Is.EqualTo(\"PromotionSet steps are already computing.\"))\n\n    [<Test>]\n    member _.ResolveConflictsRejectedWhenNotBlocked() =\n        let dto = existingPromotionSet PromotionSetStatus.Ready StepsComputationStatus.ComputeFailed\n        let metadata = createMetadata \"corr-resolve-not-blocked\"\n\n        let resolutions =\n            [\n                { FilePath = \"src/app.fs\"; Accepted = true; OverrideContentArtifactId = Option.None }\n            ]\n\n        match PromotionSet.validateCommandForState [] dto (PromotionSetCommand.ResolveConflicts(Guid.NewGuid(), resolutions)) metadata with\n        | Ok _ -> Assert.Fail(\"Expected resolve validation to fail when PromotionSet is not blocked.\")\n        | Error graceError -> Assert.That(graceError.Error, Is.EqualTo(\"PromotionSet is not blocked for conflict review.\"))\n\n    [<Test>]\n    member _.DuplicateCorrelationIdRejected() =\n        let dto = existingPromotionSet PromotionSetStatus.Ready StepsComputationStatus.Computed\n        let duplicateCorrelationId = \"corr-duplicate\"\n\n        let existingEvents: PromotionSetEvent list =\n            [\n                { Event = PromotionSetEventType.ApplyStarted; Metadata = createMetadata duplicateCorrelationId }\n            ]\n\n        match PromotionSet.validateCommandForState existingEvents dto PromotionSetCommand.Apply (createMetadata duplicateCorrelationId) with\n        | Ok _ -> Assert.Fail(\"Expected duplicate correlation ID validation to fail.\")\n        | Error graceError -> Assert.That(graceError.Error, Is.EqualTo(\"Duplicate correlation ID for PromotionSet command.\"))\n\n    [<Test>]\n    member _.UpdateInputPromotionsRejectedAfterSuccess() =\n        let dto = existingPromotionSet PromotionSetStatus.Succeeded StepsComputationStatus.Computed\n        let metadata = createMetadata \"corr-update-succeeded\"\n\n        let pointers =\n            [\n                { BranchId = Guid.NewGuid(); ReferenceId = Guid.NewGuid(); DirectoryVersionId = Guid.NewGuid() }\n            ]\n\n        match PromotionSet.validateCommandForState [] dto (PromotionSetCommand.UpdateInputPromotions pointers) metadata with\n        | Ok _ -> Assert.Fail(\"Expected update-input validation to fail for succeeded PromotionSet.\")\n        | Error graceError -> Assert.That(graceError.Error, Is.EqualTo(\"PromotionSet has already succeeded and cannot be edited.\"))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Grace.Server.Tests\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"GRACE_TEST_CLEANUP\": \"1\"\n      },\n      \"applicationUrl\": \"https://localhost:56173;http://localhost:56174\"\n    }\n  }\n}"
  },
  {
    "path": "src/Grace.Server.Tests/Queue.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server\nopen NUnit.Framework\n\n[<Parallelizable(ParallelScope.All)>]\ntype QueuePromotionSetTests() =\n    [<Test>]\n    member _.QueueInitializationRequiresPolicySnapshotWhenQueueMissing() =\n        let requiresSnapshot = Grace.Server.Queue.requiresPolicySnapshotForInitialization false \"\"\n\n        let noRequirementWhenProvided = Grace.Server.Queue.requiresPolicySnapshotForInitialization false \"snapshot\"\n\n        let noRequirementWhenQueueExists = Grace.Server.Queue.requiresPolicySnapshotForInitialization true \"\"\n\n        Assert.That(requiresSnapshot, Is.True)\n        Assert.That(noRequirementWhenProvided, Is.False)\n        Assert.That(noRequirementWhenQueueExists, Is.False)\n"
  },
  {
    "path": "src/Grace.Server.Tests/Repository.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen FSharp.Control\nopen FSharpPlus\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Microsoft.Extensions.Logging\nopen NUnit.Framework\nopen System\nopen System.Net.Http.Json\nopen System.Net\nopen System.Threading.Tasks\nopen System.IO\nopen System.Text\nopen System.Diagnostics\nopen Grace.Types.Types\nopen System.Net.Http\nopen Grace.Shared.Validation\n\n[<Parallelizable(ParallelScope.All)>]\ntype Repository() =\n\n    let log =\n        LoggerFactory\n            .Create(fun builder -> builder.AddConsole().AddDebug() |> ignore)\n            .CreateLogger(\"RepositoryTests\")\n\n    let grantRepoAdminAsync repositoryId =\n        task {\n            let parameters = Parameters.Access.GrantRoleParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.PrincipalType <- \"User\"\n            parameters.PrincipalId <- testUserId\n            parameters.ScopeKind <- \"repo\"\n            parameters.RoleId <- \"RepoAdmin\"\n            parameters.Source <- \"test\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/access/grantRole\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n        }\n\n    member val public TestContext = TestContext.CurrentContext with get, set\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithValidValues() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryDescriptionParameters()\n\n            parameters.Description <- $\"Description set at {getCurrentInstantGeneral ()}.\"\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            do! grantRepoAdminAsync parameters.RepositoryId\n\n            let! response = Client.PostAsync(\"/repository/setDescription\", createJsonContent parameters)\n            let! content = response.Content.ReadAsStringAsync()\n            response.EnsureSuccessStatusCode() |> ignore\n            Assert.That(content.Length, Is.GreaterThan(0))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithInvalidValues() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryDescriptionParameters()\n            parameters.Description <- $\"Description set at {getCurrentInstantGeneral ()}.\"\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- \"not a guid\"\n\n            let! response = Client.PostAsync(\"/repository/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! content = response.Content.ReadAsStringAsync()\n            Assert.That(content, Does.Contain(\"is not a valid Guid.\"))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetDescriptionWithEmptyDescription() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryDescriptionParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n            parameters.Description <- String.Empty\n\n            do! grantRepoAdminAsync parameters.RepositoryId\n\n            let! response = Client.PostAsync(\"/repository/setDescription\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage RepositoryError.DescriptionIsRequired))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSaveDaysWithValidValues() =\n        task {\n            let parameters = Parameters.Repository.SetSaveDaysParameters()\n            parameters.SaveDays <- 17.5f\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            let! response = Client.PostAsync(\"/repository/setSaveDays\", createJsonContent parameters)\n            let! content = response.Content.ReadAsStringAsync()\n            response.EnsureSuccessStatusCode() |> ignore\n            Assert.That(content.Length, Is.GreaterThan(0))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetSaveDaysWithInvalidValues() =\n        task {\n            let parameters = Parameters.Repository.SetSaveDaysParameters()\n            parameters.SaveDays <- -1f\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            let! response = Client.PostAsync(\"/repository/setSaveDays\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage RepositoryError.InvalidSaveDaysValue))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetCheckpointDaysWithValidValues() =\n        task {\n            let parameters = Parameters.Repository.SetCheckpointDaysParameters()\n            parameters.CheckpointDays <- 17.5f\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            let! response = Client.PostAsync(\"/repository/setCheckpointDays\", createJsonContent parameters)\n            let! content = response.Content.ReadAsStringAsync()\n            response.EnsureSuccessStatusCode() |> ignore\n            Assert.That(content.Length, Is.GreaterThan(0))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetCheckpointDaysWithInvalidValues() =\n        task {\n            let parameters = Parameters.Repository.SetCheckpointDaysParameters()\n            parameters.CheckpointDays <- -1f\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            let! response = Client.PostAsync(\"/repository/setCheckpointDays\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage RepositoryError.InvalidCheckpointDaysValue))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.GetBranchesWithValidValues() =\n        task {\n            let parameters = Parameters.Repository.GetBranchesParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            let! response = Client.PostAsync(\"/repository/getBranches\", createJsonContent parameters)\n            let! content = response.Content.ReadAsStringAsync()\n            response.EnsureSuccessStatusCode() |> ignore\n            Assert.That(content.Length, Is.GreaterThan(0))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.GetBranchesWithInvalidValues() =\n        task {\n            let parameters = Parameters.Repository.GetBranchesParameters()\n            parameters.OwnerId <- \"not a Guid\"\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n\n            let! response = Client.PostAsync(\"/repository/getBranches\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage RepositoryError.InvalidOwnerId))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetStatusWithValidValues() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryStatusParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n            parameters.Status <- \"Active\"\n\n            let! response = Client.PostAsync(\"/repository/setStatus\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<string>> response\n            let ownerGuid = Common.requireGuidProperty (nameof OwnerId) returnValue.Properties[nameof OwnerId]\n            Assert.That(ownerGuid, Is.EqualTo(Guid.Parse(ownerId)))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetStatusWithInvalidValues() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryStatusParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- \"this is an invalid OrganizationId\"\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n            parameters.Status <- \"Active\"\n\n            let! response = Client.PostAsync(\"/repository/setStatus\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage RepositoryError.InvalidOrganizationId))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetStatusWithEmptyStatus() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryStatusParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n            parameters.Status <- String.Empty\n\n            let! response = Client.PostAsync(\"/repository/setStatus\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! responseStream = response.Content.ReadAsStreamAsync()\n            let! error = deserializeAsync<GraceError> responseStream\n            Assert.That(error.Error, Is.EqualTo(getErrorMessage RepositoryError.InvalidRepositoryStatus))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetVisibilityWithValidValues() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryVisibilityParameters()\n\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n            parameters.Visibility <- \"Public\"\n\n            let! response = Client.PostAsync(\"/repository/setVisibility\", createJsonContent parameters)\n            let! content = response.Content.ReadAsStringAsync()\n            //Console.WriteLine($\"{content}\");\n            response.EnsureSuccessStatusCode() |> ignore\n            Assert.That(content.Length, Is.GreaterThan(0))\n        }\n\n    [<Test>]\n    [<Repeat(1)>]\n    member public this.SetVisibilityWithInvalidValues() =\n        task {\n            let parameters = Parameters.Repository.SetRepositoryVisibilityParameters()\n\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryIds[(rnd.Next(0, numberOfRepositories))]\n            parameters.Visibility <- \"Not a visibility value\"\n\n            let! response = Client.PostAsync(\"/repository/setVisibility\", createJsonContent parameters)\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! content = response.Content.ReadAsStringAsync()\n            //Console.WriteLine($\"{content}\");\n            Assert.That(content, Does.Contain(\"visibility\"))\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/Review.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen FsUnit\nopen Grace.Server\nopen Grace.Types.Queue\nopen Grace.Types.Review\nopen Grace.Shared.Parameters.Review\nopen Grace.Types.PromotionSet\nopen NUnit.Framework\nopen System\nopen System.Threading.Tasks\n\n[<Parallelizable(ParallelScope.All)>]\ntype ReviewProjectionTests() =\n    let createParameters (candidateId: string) =\n        let parameters = ResolveCandidateIdentityParameters(CandidateId = candidateId)\n        parameters.OwnerId <- Guid.NewGuid().ToString()\n        parameters.OrganizationId <- Guid.NewGuid().ToString()\n        parameters.RepositoryId <- Guid.NewGuid().ToString()\n        parameters.CorrelationId <- Guid.NewGuid().ToString()\n        parameters\n\n    [<Test>]\n    member _.ResolveCandidateIdentityProjectionWithUsesDirectPromotionSetProjection() =\n        let candidateId = Guid.NewGuid()\n        let parameters = createParameters $\"  {candidateId.ToString().ToUpperInvariant()}  \"\n\n        let resolvePromotionSet (promotionSetId: Guid) : Task<Grace.Types.PromotionSet.PromotionSetDto option> =\n            task {\n                let promotionSet = { PromotionSetDto.Default with PromotionSetId = promotionSetId }\n                return Option.Some promotionSet\n            }\n\n        let resultTask = Review.resolveCandidateIdentityProjectionWith resolvePromotionSet parameters\n        let result = resultTask.GetAwaiter().GetResult()\n\n        match result with\n        | Error error -> Assert.Fail($\"Expected projection resolution to succeed, but received error: {error.Error}\")\n        | Ok projection ->\n            Assert.That(projection.Identity.CandidateId, Is.EqualTo(candidateId.ToString()))\n            Assert.That(projection.Identity.PromotionSetId, Is.EqualTo(candidateId.ToString()))\n            Assert.That(projection.Identity.IdentityMode, Is.EqualTo(CandidateIdentityModes.DirectPromotionSetProjection))\n            Assert.That(projection.Identity.Scope.OwnerId, Is.EqualTo(parameters.OwnerId))\n            Assert.That(projection.Identity.Scope.OrganizationId, Is.EqualTo(parameters.OrganizationId))\n            Assert.That(projection.Identity.Scope.RepositoryId, Is.EqualTo(parameters.RepositoryId))\n            Assert.That(projection.SourceStates.Length, Is.EqualTo(1))\n            Assert.That(projection.SourceStates[0].Section, Is.EqualTo(\"identity\"))\n            Assert.That(projection.SourceStates[0].SourceState, Is.EqualTo(ProjectionSourceStates.Authoritative))\n\n    [<Test>]\n    member _.ResolveCandidateIdentityProjectionWithReturnsDeterministicProjectionForSameInput() =\n        let candidateId = Guid.NewGuid()\n        let parameters = createParameters (candidateId.ToString())\n\n        let resolvePromotionSet (promotionSetId: Guid) : Task<Grace.Types.PromotionSet.PromotionSetDto option> =\n            task {\n                let promotionSet = { PromotionSetDto.Default with PromotionSetId = promotionSetId }\n                return Option.Some promotionSet\n            }\n\n        let firstResultTask = Review.resolveCandidateIdentityProjectionWith resolvePromotionSet parameters\n        let firstResult = firstResultTask.GetAwaiter().GetResult()\n\n        let secondResultTask = Review.resolveCandidateIdentityProjectionWith resolvePromotionSet parameters\n        let secondResult = secondResultTask.GetAwaiter().GetResult()\n\n        let getDeterministicShape (result: Result<CandidateIdentityProjectionResult, Grace.Types.Types.GraceError>) =\n            match result with\n            | Error error -> $\"error:{error.Error}:{error.CorrelationId}:{error.Properties.Count}\"\n            | Ok projection ->\n                String.Join(\n                    \"|\",\n                    [\n                        projection.Identity.CandidateId\n                        projection.Identity.PromotionSetId\n                        projection.Identity.IdentityMode\n                        projection.Identity.Scope.OwnerId\n                        projection.Identity.Scope.OrganizationId\n                        projection.Identity.Scope.RepositoryId\n                        projection.SourceStates\n                        |> List.map (fun source -> $\"{source.Section}:{source.SourceState}:{source.Detail}\")\n                        |> String.concat \";\"\n                    ]\n                )\n\n        Assert.That(getDeterministicShape firstResult, Is.EqualTo(getDeterministicShape secondResult))\n\n    [<Test>]\n    member _.ResolveCandidateIdentityProjectionWithRejectsInvalidCandidateIdWithoutLookup() =\n        let parameters = createParameters \"  not-a-guid  \"\n        let mutable resolveCalls = 0\n\n        let resolvePromotionSet (_: Guid) : Task<Grace.Types.PromotionSet.PromotionSetDto option> =\n            task {\n                resolveCalls <- resolveCalls + 1\n                return Option.None\n            }\n\n        let resultTask = Review.resolveCandidateIdentityProjectionWith resolvePromotionSet parameters\n        let result = resultTask.GetAwaiter().GetResult()\n\n        match result with\n        | Ok _ -> Assert.Fail(\"Expected invalid candidate id to return an error.\")\n        | Error error ->\n            Assert.That(resolveCalls, Is.EqualTo(0))\n            Assert.That(error.Error, Is.EqualTo(\"CandidateId must be a valid non-empty Guid.\"))\n            Assert.That(error.CorrelationId, Is.EqualTo(parameters.CorrelationId))\n            Assert.That(error.Properties.ContainsKey(\"NormalizedCandidateId\"), Is.True)\n            Assert.That(error.Properties[\"NormalizedCandidateId\"], Is.EqualTo(\"not-a-guid\"))\n\n    [<Test>]\n    member _.ResolveCandidateIdentityProjectionWithReturnsDeterministicNotFoundError() =\n        let candidateId = Guid.NewGuid()\n        let parameters = createParameters (candidateId.ToString())\n\n        let resolvePromotionSet (_: Guid) : Task<Grace.Types.PromotionSet.PromotionSetDto option> = task { return Option.None }\n\n        let resultTask = Review.resolveCandidateIdentityProjectionWith resolvePromotionSet parameters\n        let result = resultTask.GetAwaiter().GetResult()\n\n        match result with\n        | Ok _ -> Assert.Fail(\"Expected missing candidate projection to return an error.\")\n        | Error error ->\n            Assert.That(error.Error, Is.EqualTo($\"Candidate '{candidateId}' was not found in repository scope.\"))\n            Assert.That(error.CorrelationId, Is.EqualTo(parameters.CorrelationId))\n            Assert.That(error.Properties.ContainsKey(\"RepositoryId\"), Is.True)\n            Assert.That(error.Properties[\"RepositoryId\"], Is.EqualTo(parameters.RepositoryId))\n            Assert.That(error.Properties[\"NormalizedCandidateId\"], Is.EqualTo(candidateId.ToString()))\n\n    [<Test>]\n    member _.DeriveCandidateRequiredActionsReturnsOrderedDeterministicActions() =\n        let actions, diagnostics =\n            Review.deriveCandidateRequiredActions PromotionSetStatus.Blocked StepsComputationStatus.ComputeFailed (Option.Some QueueState.Paused) 2 false\n\n        actions\n        |> should\n            equal\n            [\n                \"RetryComputation\"\n                \"ResolveConflicts\"\n                \"ResumeQueue\"\n                \"ResolveFindings\"\n                \"ConfirmValidationSummary\"\n            ]\n\n        Assert.That(diagnostics, Is.Empty)\n\n    [<Test>]\n    member _.BuildCandidateProjectionSnapshotIncludesNotAvailableDiagnostics() =\n        let promotionSet =\n            { PromotionSetDto.Default with\n                PromotionSetId = Guid.NewGuid()\n                Status = PromotionSetStatus.Ready\n                StepsComputationStatus = StepsComputationStatus.Computed\n            }\n\n        let identity = CandidateIdentityProjection()\n        identity.CandidateId <- promotionSet.PromotionSetId.ToString()\n        identity.PromotionSetId <- promotionSet.PromotionSetId.ToString()\n        identity.IdentityMode <- CandidateIdentityModes.DirectPromotionSetProjection\n        let scope = CandidateProjectionScope()\n        scope.OwnerId <- Guid.NewGuid().ToString()\n        scope.OrganizationId <- Guid.NewGuid().ToString()\n        scope.RepositoryId <- Guid.NewGuid().ToString()\n        identity.Scope <- scope\n\n        let snapshot = Review.buildCandidateProjectionSnapshot identity promotionSet Option.None Option.None\n\n        snapshot.RequiredActions\n        |> should equal [ \"ConfirmValidationSummary\" ]\n\n        snapshot.Diagnostics\n        |> should\n            equal\n            [\n                \"Queue state is unavailable for this candidate.\"\n                \"Review notes are not available for this candidate.\"\n            ]\n\n        Assert.That(snapshot.QueueState, Is.EqualTo(ProjectionSourceStates.NotAvailable))\n\n        snapshot.SourceStates\n        |> List.map (fun source -> source.Section)\n        |> should\n            equal\n            [\n                \"identity\"\n                \"promotionSet\"\n                \"queue\"\n                \"review\"\n            ]\n\n    [<Test>]\n    member _.BuildCandidateAttestationEntriesMarksMissingSourcesAsNotAvailable() =\n        let attestations, diagnostics, sourceStates = Review.buildCandidateAttestationEntries Option.None Option.None\n\n        Assert.That(attestations.Length, Is.EqualTo(2))\n        Assert.That(attestations[0].Name, Is.EqualTo(\"PolicySnapshot\"))\n        Assert.That(attestations[0].Status, Is.EqualTo(ProjectionSourceStates.NotAvailable))\n        Assert.That(attestations[1].Name, Is.EqualTo(\"ReviewCheckpoint\"))\n        Assert.That(attestations[1].Status, Is.EqualTo(ProjectionSourceStates.NotAvailable))\n\n        diagnostics\n        |> should\n            equal\n            [\n                \"Policy snapshot context is unavailable for this candidate.\"\n                \"Review checkpoint context is unavailable for this candidate.\"\n            ]\n\n        sourceStates\n        |> List.map (fun source -> source.Section)\n        |> should equal [ \"identity\"; \"policy\"; \"checkpoint\" ]\n"
  },
  {
    "path": "src/Grace.Server.Tests/ReviewNotes.Determinism.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Shared\nopen Grace.Shared.ReviewNotes\nopen Grace.Types.Policy\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen NUnit.Framework\nopen System\n\n[<Parallelizable(ParallelScope.All)>]\ntype ReviewNotesDeterminism() =\n    let evidenceSummary path = { RelativePath = path; StartLine = 1; EndLine = 2; Score = 1.0; Reasons = [] }\n\n    [<Test>]\n    member _.ChapteringIsDeterministicAcrossOrdering() =\n        let paths =\n            [\n                \"src/App.fs\"\n                \"docs/readme.md\"\n                \"src/Utils.fs\"\n                \"docs/guide.md\"\n            ]\n\n        let shuffled =\n            [\n                \"docs/guide.md\"\n                \"src/Utils.fs\"\n                \"docs/readme.md\"\n                \"src/App.fs\"\n            ]\n\n        let evidence = paths |> List.map evidenceSummary\n\n        let first = buildChapters paths evidence\n        let second = buildChapters shuffled evidence\n\n        let matches = first = second\n        Assert.That(matches, Is.True)\n\n    [<Test>]\n    member _.ChaptersUseTopLevelPathSegment() =\n        let paths =\n            [\n                \"README.md\"\n                \"src/App.fs\"\n                \"src/Utils.fs\"\n                \"docs/guide.md\"\n            ]\n\n        let evidence = paths |> List.map evidenceSummary\n\n        let chapters = buildChapters paths evidence\n\n        let readmeChapter =\n            chapters\n            |> List.find (fun chapter -> chapter.Title = \"README.md\")\n\n        let srcChapter =\n            chapters\n            |> List.find (fun chapter -> chapter.Title = \"src\")\n\n        let docsChapter =\n            chapters\n            |> List.find (fun chapter -> chapter.Title = \"docs\")\n\n        let readmeMatch = readmeChapter.Paths = [ \"README.md\" ]\n        let srcMatch = srcChapter.Paths = [ \"src/App.fs\"; \"src/Utils.fs\" ]\n        let docsMatch = docsChapter.Paths = [ \"docs/guide.md\" ]\n\n        Assert.That(readmeMatch, Is.True)\n        Assert.That(srcMatch, Is.True)\n        Assert.That(docsMatch, Is.True)\n\n    [<Test>]\n    member _.ChapterIdsStayStableWhenEvidenceOrderingChanges() =\n        let paths = [ \"src/App.fs\"; \"docs/readme.md\" ]\n        let evidenceOne = paths |> List.map evidenceSummary\n\n        let evidenceTwo =\n            [ \"docs/readme.md\"; \"src/App.fs\" ]\n            |> List.map evidenceSummary\n\n        let chapterIdsFirst =\n            buildChapters paths evidenceOne\n            |> List.map (fun chapter -> chapter.ChapterId)\n\n        let chapterIdsSecond =\n            buildChapters paths evidenceTwo\n            |> List.map (fun chapter -> chapter.ChapterId)\n\n        let idsMatch = chapterIdsFirst = chapterIdsSecond\n        Assert.That(idsMatch, Is.True)\n\n    [<Test>]\n    member _.BaselineDriftTargetsAffectedChaptersAndFindings() =\n        let threshold = { ChurnLines = 1; FilesTouched = 1 }\n        let policy = { PolicySnapshot.Default with Rules = { PolicySnapshot.Default.Rules with ApprovalRules = { BaselineDriftReackThreshold = threshold } } }\n\n        let changedPath = \"src/Auth.fs\"\n\n        let riskProfile =\n            { DeterministicRiskProfile.Default with\n                ChangedPaths =\n                    [\n                        { RelativePath = changedPath; ChangeType = PathChangeType.Modified }\n                    ]\n                Churn = { LinesAdded = 10; LinesRemoved = 0; FilesChanged = 1; RenamedCount = 0 }\n            }\n\n        let chapters =\n            buildChapters [ changedPath ] [\n                evidenceSummary changedPath\n            ]\n\n        let chapterId =\n            chapters\n            |> List.head\n            |> (fun chapter -> chapter.ChapterId)\n\n        let findingId = Guid.NewGuid()\n\n        let finding =\n            {\n                FindingId = findingId\n                Severity = FindingSeverity.High\n                Category = FindingCategory.Security\n                Description = \"Risk\"\n                Rationale = \"Rationale\"\n                RequiredActionType = \"Review\"\n                EvidenceReferences =\n                    [\n                        { RelativePath = changedPath; StartLine = 1; EndLine = 2 }\n                    ]\n                ResolutionState = FindingResolutionState.Open\n                ResolvedBy = None\n                ResolvedAt = None\n                ResolutionNote = None\n            }\n\n        let result = BaselineDrift.evaluate policy riskProfile chapters [ finding ]\n\n        Assert.That(result.IsMeaningful, Is.True)\n        Assert.That(result.AffectedChapterIds, Does.Contain(chapterId))\n        Assert.That(result.AffectedFindingIds, Does.Contain(findingId))\n\n    [<Test>]\n    member _.BaselineDriftBelowThresholdIsNotMeaningful() =\n        let threshold = { ChurnLines = 10; FilesTouched = 5 }\n        let policy = { PolicySnapshot.Default with Rules = { PolicySnapshot.Default.Rules with ApprovalRules = { BaselineDriftReackThreshold = threshold } } }\n\n        let riskProfile =\n            { DeterministicRiskProfile.Default with\n                ChangedPaths =\n                    [\n                        { RelativePath = \"src/Auth.fs\"; ChangeType = PathChangeType.Modified }\n                    ]\n                Churn = { LinesAdded = 1; LinesRemoved = 1; FilesChanged = 1; RenamedCount = 0 }\n            }\n\n        let chapters =\n            buildChapters [ \"src/Auth.fs\" ] [\n                evidenceSummary \"src/Auth.fs\"\n            ]\n\n        let result = BaselineDrift.evaluate policy riskProfile chapters []\n\n        Assert.That(result.IsMeaningful, Is.False)\n\n    [<Test>]\n    member _.BaselineDriftSkipsChaptersAndFindingsWithoutMatchingPaths() =\n        let threshold = { ChurnLines = 1; FilesTouched = 1 }\n        let policy = { PolicySnapshot.Default with Rules = { PolicySnapshot.Default.Rules with ApprovalRules = { BaselineDriftReackThreshold = threshold } } }\n\n        let riskProfile =\n            { DeterministicRiskProfile.Default with\n                ChangedPaths =\n                    [\n                        { RelativePath = \"src/Auth.fs\"; ChangeType = PathChangeType.Modified }\n                    ]\n                Churn = { LinesAdded = 5; LinesRemoved = 0; FilesChanged = 1; RenamedCount = 0 }\n            }\n\n        let chapters =\n            buildChapters [ \"docs/readme.md\" ] [\n                evidenceSummary \"docs/readme.md\"\n            ]\n\n        let finding =\n            {\n                FindingId = Guid.NewGuid()\n                Severity = FindingSeverity.Low\n                Category = FindingCategory.Tests\n                Description = \"No match\"\n                Rationale = \"No match\"\n                RequiredActionType = \"Review\"\n                EvidenceReferences =\n                    [\n                        { RelativePath = \"docs/readme.md\"; StartLine = 1; EndLine = 2 }\n                    ]\n                ResolutionState = FindingResolutionState.Open\n                ResolvedBy = None\n                ResolvedAt = None\n                ResolutionNote = None\n            }\n\n        let result = BaselineDrift.evaluate policy riskProfile chapters [ finding ]\n\n        Assert.That(result.AffectedChapterIds, Is.Empty)\n        Assert.That(result.AffectedFindingIds, Is.Empty)\n\n    [<Test>]\n    member _.BaselineDriftIsMeaningfulAtThreshold() =\n        let threshold = { ChurnLines = 4; FilesTouched = 2 }\n        let policy = { PolicySnapshot.Default with Rules = { PolicySnapshot.Default.Rules with ApprovalRules = { BaselineDriftReackThreshold = threshold } } }\n\n        let riskProfile =\n            { DeterministicRiskProfile.Default with\n                ChangedPaths =\n                    [\n                        { RelativePath = \"src/Auth.fs\"; ChangeType = PathChangeType.Modified }\n                    ]\n                Churn = { LinesAdded = 4; LinesRemoved = 0; FilesChanged = 0; RenamedCount = 0 }\n            }\n\n        let chapters =\n            buildChapters [ \"src/Auth.fs\" ] [\n                evidenceSummary \"src/Auth.fs\"\n            ]\n\n        let result = BaselineDrift.evaluate policy riskProfile chapters []\n\n        Assert.That(result.IsMeaningful, Is.True)\n"
  },
  {
    "path": "src/Grace.Server.Tests/Services.EffectivePromotion.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Actors\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen NodaTime\nopen NUnit.Framework\nopen System\n\n[<Parallelizable(ParallelScope.All)>]\ntype ServicesEffectivePromotionTests() =\n\n    let createPromotionReference createdAt isDeleted hasTerminalLink =\n        let links: ReferenceLinkType seq =\n            if hasTerminalLink then\n                [\n                    ReferenceLinkType.PromotionSetTerminal(Guid.NewGuid())\n                ]\n            else\n                [\n                    ReferenceLinkType.IncludedInPromotionSet(Guid.NewGuid())\n                ]\n\n        { ReferenceDto.Default with\n            ReferenceId = Guid.NewGuid()\n            BranchId = Guid.NewGuid()\n            DirectoryId = Guid.NewGuid()\n            ReferenceType = ReferenceType.Promotion\n            CreatedAt = createdAt\n            Links = links\n            DeletedAt = if isDeleted then Some(createdAt + Duration.FromMinutes(1.0)) else Option.None\n        }\n\n    [<Test>]\n    member _.LatestEffectivePromotionIgnoresDeletedTerminalReferences() =\n        let createdAt = Instant.FromUtc(2026, 2, 21, 10, 0)\n\n        let deletedLatestTerminal = createPromotionReference (createdAt + Duration.FromMinutes(3.0)) true true\n        let latestNonTerminal = createPromotionReference (createdAt + Duration.FromMinutes(2.0)) false false\n        let previousTerminal = createPromotionReference (createdAt + Duration.FromMinutes(1.0)) false true\n\n        let selected =\n            Services.tryGetLatestEffectivePromotionReference (\n                [\n                    deletedLatestTerminal\n                    latestNonTerminal\n                    previousTerminal\n                ]\n            )\n\n        Assert.That(selected, Is.EqualTo(Some previousTerminal))\n\n    [<Test>]\n    member _.LatestEffectivePromotionRequiresTerminalLink() =\n        let createdAt = Instant.FromUtc(2026, 2, 21, 12, 0)\n        let nonTerminal = createPromotionReference createdAt false false\n\n        let selected = Services.tryGetLatestEffectivePromotionReference ([ nonTerminal ])\n\n        Assert.That(selected, Is.EqualTo(None))\n\n    [<Test>]\n    member _.LatestReferenceSelectionIgnoresDeletedReferences() =\n        let createdAt = Instant.FromUtc(2026, 2, 21, 14, 0)\n        let deletedLatest = createPromotionReference (createdAt + Duration.FromMinutes(2.0)) true true\n        let previousActive = createPromotionReference (createdAt + Duration.FromMinutes(1.0)) false false\n\n        let selected = Services.tryGetLatestNotDeletedReference ([ deletedLatest; previousActive ])\n\n        Assert.That(selected, Is.EqualTo(Some previousActive))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Slop.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Grace.Types.Owner\nopen NUnit.Framework\nopen System\nopen System.Net.Http\nopen System.Text.Json\n\n[<TestFixture>]\ntype Slop() =\n    // Slop guard: if correlation middleware is removed or renamed, this breaks.\n    [<Test; Category(\"Slop\")>]\n    member _.OwnerGetEchoesCorrelationIdHeader() =\n        task {\n            let correlationId = generateCorrelationId ()\n            let parameters = Parameters.Owner.GetOwnerParameters()\n            parameters.OwnerId <- Services.ownerId\n            parameters.CorrelationId <- correlationId\n\n            use request = new HttpRequestMessage(HttpMethod.Post, \"/owner/get\")\n            request.Headers.Add(Constants.CorrelationIdHeaderKey, correlationId)\n            request.Content <- createJsonContent parameters\n\n            let! response = Services.Client.SendAsync(request)\n            response.EnsureSuccessStatusCode() |> ignore\n\n            Assert.That(response.Headers.Contains(Constants.CorrelationIdHeaderKey), Is.True)\n\n            let echoed =\n                response.Headers.GetValues(Constants.CorrelationIdHeaderKey)\n                |> Seq.head\n\n            Assert.That(echoed, Is.EqualTo(correlationId))\n        }\n\n    // Slop guard: if OwnerDto.Class or response shape changes, this breaks.\n    [<Test; Category(\"Slop\")>]\n    member _.OwnerGetReturnsOwnerDtoClass() =\n        task {\n            let parameters = Parameters.Owner.GetOwnerParameters()\n            parameters.OwnerId <- Services.ownerId\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Services.Client.PostAsync(\"/owner/get\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n\n            let! payload = deserializeContent<GraceReturnValue<OwnerDto>> response\n            Assert.That(payload.ReturnValue.Class, Is.EqualTo(nameof OwnerDto))\n        }\n\n    // Slop guard: if JSON serialization options change, round-tripping breaks.\n    [<Test; Category(\"Slop\")>]\n    member _.OwnerDtoJsonRoundTrip() =\n        let original = { OwnerDto.Default with OwnerId = Guid.NewGuid(); OwnerName = \"SlopOwner\" }\n\n        let json = JsonSerializer.Serialize(original, Constants.JsonSerializerOptions)\n        let roundTrip = JsonSerializer.Deserialize<OwnerDto>(json, Constants.JsonSerializerOptions)\n        Assert.That(roundTrip, Is.EqualTo(original))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Smoke.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen NUnit.Framework\n\n[<TestFixture>]\n[<NonParallelizable>]\ntype Smoke() =\n    [<Test; Category(\"Smoke\")>]\n    member _.HealthzReturnsSuccess() =\n        task {\n            let! response = Services.Client.GetAsync(\"/healthz\")\n            let! body = response.Content.ReadAsStringAsync()\n            Assert.That(response.IsSuccessStatusCode, Is.True, \"Expected /healthz to return success.\")\n            Assert.That(body, Does.Contain(\"healthy\").IgnoreCase, \"Expected /healthz body to indicate health.\")\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/Validation.Artifact.Contract.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Server\nopen Grace.Shared.Parameters.Validation\nopen Grace.Shared.Validation\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Artifact\nopen NodaTime\nopen NUnit.Framework\nopen System\n\n[<Parallelizable(ParallelScope.All)>]\ntype ValidationArtifactContractTests() =\n\n    [<Test>]\n    member _.ArtifactBlobPathMatchesContract() =\n        let artifactId = Guid.Parse(\"78a3f80f-6dd4-4a38-b005-5178bc65f9cd\")\n        let timestamp = Instant.FromUtc(2026, 2, 19, 13, 0)\n\n        let blobPath = Artifact.buildBlobPath timestamp artifactId\n\n        Assert.That(blobPath, Is.EqualTo(\"grace-artifacts/2026/02/19/13/78a3f80f-6dd4-4a38-b005-5178bc65f9cd\"))\n\n    [<Test>]\n    member _.UnknownArtifactTypeMapsToOther() =\n        let parsed = Artifact.parseArtifactType \"CustomOutput\"\n\n        Assert.That(parsed, Is.EqualTo(ArtifactType.Other \"CustomOutput\"))\n\n    [<Test>]\n    member _.ValidationResultRequiresStepsComputationAttemptWhenPromotionSetScopeIsProvided() =\n        let parameters = RecordValidationResultParameters()\n        parameters.ValidationName <- \"quick-scan\"\n        parameters.ValidationVersion <- \"1.0\"\n        parameters.Status <- \"Pass\"\n        parameters.PromotionSetId <- Guid.NewGuid().ToString()\n        parameters.StepsComputationAttempt <- -1\n\n        let firstError =\n            ValidationResult.validationsForRecord parameters\n            |> Utilities.getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(firstError, Is.EqualTo(Some ValidationResultError.StepsComputationAttemptRequired))\n\n    [<Test>]\n    member _.ValidationResultAcceptsValidPromotionSetScope() =\n        let parameters = RecordValidationResultParameters()\n        parameters.ValidationName <- \"quick-scan\"\n        parameters.ValidationVersion <- \"1.0\"\n        parameters.Status <- \"Pass\"\n        parameters.PromotionSetId <- Guid.NewGuid().ToString()\n        parameters.StepsComputationAttempt <- 2\n\n        let firstError =\n            ValidationResult.validationsForRecord parameters\n            |> Utilities.getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(firstError, Is.EqualTo(None))\n"
  },
  {
    "path": "src/Grace.Server.Tests/Validations.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Grace.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Common\nopen Grace.Shared.Validation.Errors\nopen FsUnit\nopen Microsoft.FSharp.Core\nopen NUnit.Framework\nopen System\nopen System.Threading.Tasks\n\n[<Parallelizable(ParallelScope.All)>]\ntype Validations() =\n\n    [<Test>]\n    member this.``valid Guid returns Ok``() =\n        let result =\n            (Guid.isValidAndNotEmptyGuid \"6fddb3c1-24c2-4e2e-8f57-98d0838c0c3f\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``empty string for guid returns Ok``() =\n        let result =\n            (Guid.isValidAndNotEmptyGuid \"\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``invalid Guid returns Error``() =\n        let result =\n            (Guid.isValidAndNotEmptyGuid \"not a Guid\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``Guid Empty returns Error``() =\n        let result =\n            (Guid.isValidAndNotEmptyGuid (Guid.Empty.ToString()) TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``Guid is not Guid Empty returns Ok``() =\n        let result =\n            (Guid.isNotEmpty (Guid.NewGuid()) TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``Guid is Guid Empty returns Error``() =\n        let result =\n            (Guid.isNotEmpty Guid.Empty TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``positive number returns Ok``() =\n        let result =\n            (Number.isPositiveOrZero 5.0 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``zero returns Ok``() =\n        let result =\n            (Number.isPositiveOrZero 0.0 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``negative number returns Error``() =\n        let result =\n            (Number.isPositiveOrZero (-5.0) TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``number within range returns Ok``() =\n        let result =\n            (Number.isWithinRange 4 0 10 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``number not within range returns Error``() =\n        let result =\n            (Number.isWithinRange 20 0 10 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``not empty string returns Ok``() =\n        let result =\n            (String.isNotEmpty \"not empty\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``empty string returns Error``() =\n        let result = (String.isNotEmpty \"\" TestError.TestFailed).Result\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``valid SHA-256 hash returns Ok``() =\n        let result =\n            (String.isEmptyOrValidSha256Hash \"67A1790DCA55B8803AD024EE28F616A284DF5DD7B8BA5F68B4B252A5E925AF79\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``empty string for SHA-256 value returns Ok``() =\n        let result =\n            (String.isEmptyOrValidSha256Hash \"\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``invalid SHA-256 hash returns Error``() =\n        let result =\n            (String.isValidSha256Hash \"not a SHA-256 hash\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``string length less than max length returns Ok``() =\n        let result =\n            (String.maxLength \"a string\" 10 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``string length equal to max length returns Ok``() =\n        let result =\n            (String.maxLength \"a string\" 8 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``string length greater than max length returns Error``() =\n        let result =\n            (String.maxLength \"a string\" 1 TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``string is member of discriminated union returns Ok``() =\n        let result =\n            (DiscriminatedUnion.isMemberOf<Types.ReferenceType, TestError> \"Checkpoint\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``string is not member of discriminated union returns Error``() =\n        let result =\n            (DiscriminatedUnion.isMemberOf<Types.ReferenceType, TestError> \"Not a member\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``either id or name is provided returns Ok``() =\n        let result =\n            (Input.eitherIdOrNameMustBeProvided \"id\" \"\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``both id and name are provided returns Ok``() =\n        let result =\n            (Input.eitherIdOrNameMustBeProvided \"id\" \"name\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``neither id nor name is provided returns Error``() =\n        let result =\n            (Input.eitherIdOrNameMustBeProvided \"\" \"\" TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n\n    [<Test>]\n    member this.``non-empty list returns Ok``() =\n        let result =\n            (Input.listIsNonEmpty [ 1; 2; 3 ] TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.okResult))\n\n    [<Test>]\n    member this.``empty list returns Error``() =\n        let result =\n            (Input.listIsNonEmpty [] TestError.TestFailed)\n                .Result\n\n        Assert.That(result, Is.EqualTo(Common.errorResult))\n"
  },
  {
    "path": "src/Grace.Server.Tests/WorkItem.Integration.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen Azure.Storage.Blobs\nopen Grace.Server.Tests.Services\nopen Grace.Shared\nopen Grace.Shared.Client.Configuration\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Artifact\nopen Grace.Types.PersonalAccessToken\nopen Grace.Types.Types\nopen Grace.Types.WorkItem\nopen NUnit.Framework\nopen System\nopen System.Net\nopen System.Net.Http\nopen System.Net.Http.Headers\nopen System.Threading.Tasks\n\nmodule private WorkItemIntegrationHelpers =\n    let createAuthenticatedClient (userId: string) =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client.DefaultRequestHeaders.Add(\"x-grace-user-id\", userId)\n        client\n\n    let createUnauthenticatedClient () =\n        let client = new HttpClient()\n        client.BaseAddress <- Client.BaseAddress\n        client\n\n    let createRepositoryAsync (repositoryNamePrefix: string) =\n        task {\n            let repositoryId = Guid.NewGuid().ToString()\n            let parameters = Parameters.Repository.CreateRepositoryParameters()\n\n            let prefix =\n                if String.IsNullOrWhiteSpace(repositoryNamePrefix) then\n                    \"repo\"\n                else\n                    repositoryNamePrefix.Trim()\n\n            let maxPrefixLength = 31\n\n            let boundedPrefix =\n                if prefix.Length > maxPrefixLength then\n                    prefix.Substring(0, maxPrefixLength)\n                else\n                    prefix\n\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.RepositoryName <- $\"{boundedPrefix}-{Guid.NewGuid():N}\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/repository/create\", createJsonContent parameters)\n\n            if not response.IsSuccessStatusCode then\n                let! body = response.Content.ReadAsStringAsync()\n                Assert.Fail($\"Expected repository create success but got {(int response.StatusCode)} {response.StatusCode}: {body}\")\n            else\n                ()\n\n            let storageConnectionString = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.AzureStorageConnectionString)\n\n            if not (String.IsNullOrWhiteSpace(storageConnectionString)) then\n                let serviceClient = BlobServiceClient(storageConnectionString)\n                let containerClient = serviceClient.GetBlobContainerClient(repositoryId.ToLowerInvariant())\n                let! _ = containerClient.CreateIfNotExistsAsync()\n                ()\n\n            return repositoryId\n        }\n\n    let createWorkItemAsync (repositoryId: string) (title: string) =\n        task {\n            let workItemId = Guid.NewGuid().ToString()\n            let parameters = Parameters.WorkItem.CreateWorkItemParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemId\n            parameters.Title <- title\n            parameters.Description <- \"integration test work item\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/work/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            return workItemId\n        }\n\n    let getWorkItemResponseAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) =\n        task {\n            let parameters = Parameters.WorkItem.GetWorkItemParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/get\", createJsonContent parameters)\n        }\n\n    let getWorkItemDtoAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) =\n        task {\n            let! response = getWorkItemResponseAsync client repositoryId workItemIdentifier\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<WorkItemDto>> response\n            return returnValue.ReturnValue\n        }\n\n    let getWorkItemLinksAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) =\n        task {\n            let parameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = client.PostAsync(\"/work/links/list\", createJsonContent parameters)\n\n            if not response.IsSuccessStatusCode then\n                let! body = response.Content.ReadAsStringAsync()\n                Assert.Fail($\"Expected links/list success but got {(int response.StatusCode)} {response.StatusCode}: {body}\")\n                return WorkItemLinksDto.Default\n            else\n                let! returnValue = deserializeContent<GraceReturnValue<WorkItemLinksDto>> response\n                return returnValue.ReturnValue\n        }\n\n    let listWorkItemAttachmentsResponseAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) =\n        task {\n            let parameters = Parameters.WorkItem.ListWorkItemAttachmentsParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/attachments/list\", createJsonContent parameters)\n        }\n\n    let listWorkItemAttachmentsAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) =\n        task {\n            let! response = listWorkItemAttachmentsResponseAsync client repositoryId workItemIdentifier\n\n            if not response.IsSuccessStatusCode then\n                let! body = response.Content.ReadAsStringAsync()\n                Assert.Fail($\"Expected attachments/list success but got {(int response.StatusCode)} {response.StatusCode}: {body}\")\n                return Parameters.WorkItem.ListWorkItemAttachmentsResult()\n            else\n                let! returnValue = deserializeContent<GraceReturnValue<Parameters.WorkItem.ListWorkItemAttachmentsResult>> response\n                return returnValue.ReturnValue\n        }\n\n    let showWorkItemAttachmentResponseAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (attachmentType: string) (latest: bool) =\n        task {\n            let parameters = Parameters.WorkItem.ShowWorkItemAttachmentParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.AttachmentType <- attachmentType\n            parameters.Latest <- latest\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/attachments/show\", createJsonContent parameters)\n        }\n\n    let showWorkItemAttachmentAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (attachmentType: string) (latest: bool) =\n        task {\n            let! response = showWorkItemAttachmentResponseAsync client repositoryId workItemIdentifier attachmentType latest\n\n            if not response.IsSuccessStatusCode then\n                let! body = response.Content.ReadAsStringAsync()\n                Assert.Fail($\"Expected attachments/show success but got {(int response.StatusCode)} {response.StatusCode}: {body}\")\n                return Parameters.WorkItem.ShowWorkItemAttachmentResult()\n            else\n                let! returnValue = deserializeContent<GraceReturnValue<Parameters.WorkItem.ShowWorkItemAttachmentResult>> response\n                return returnValue.ReturnValue\n        }\n\n    let downloadWorkItemAttachmentResponseAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (artifactId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.DownloadWorkItemAttachmentParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.ArtifactId <- artifactId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/attachments/download\", createJsonContent parameters)\n        }\n\n    let downloadWorkItemAttachmentAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (artifactId: Guid) =\n        task {\n            let! response = downloadWorkItemAttachmentResponseAsync client repositoryId workItemIdentifier artifactId\n\n            if not response.IsSuccessStatusCode then\n                let! body = response.Content.ReadAsStringAsync()\n                Assert.Fail($\"Expected attachments/download success but got {(int response.StatusCode)} {response.StatusCode}: {body}\")\n                return Parameters.WorkItem.DownloadWorkItemAttachmentResult()\n            else\n                let! returnValue = deserializeContent<GraceReturnValue<Parameters.WorkItem.DownloadWorkItemAttachmentResult>> response\n                return returnValue.ReturnValue\n        }\n\n    let addSummaryResponseAsync\n        (client: HttpClient)\n        (repositoryId: string)\n        (workItemIdentifier: string)\n        (summaryContent: string)\n        (promptContent: string option)\n        (promptOrigin: string option)\n        (summaryArtifactIdOverride: string option)\n        (correlationId: string)\n        =\n        task {\n            let parameters = Parameters.WorkItem.AddSummaryParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.SummaryContent <- summaryContent\n            parameters.SummaryMimeType <- \"text/markdown\"\n            parameters.PromptContent <- promptContent |> Option.defaultValue String.Empty\n            parameters.PromptMimeType <- if promptContent.IsSome then \"text/markdown\" else String.Empty\n            parameters.PromptOrigin <- promptOrigin |> Option.defaultValue String.Empty\n\n            parameters.SummaryArtifactId <-\n                summaryArtifactIdOverride\n                |> Option.defaultValue String.Empty\n\n            parameters.CorrelationId <- correlationId\n            return! client.PostAsync(\"/work/add-summary\", createJsonContent parameters)\n        }\n\n    let addSummaryAsync\n        (client: HttpClient)\n        (repositoryId: string)\n        (workItemIdentifier: string)\n        (summaryContent: string)\n        (promptContent: string option)\n        (promptOrigin: string option)\n        (correlationId: string)\n        =\n        task {\n            let! response = addSummaryResponseAsync client repositoryId workItemIdentifier summaryContent promptContent promptOrigin None correlationId\n\n            if not response.IsSuccessStatusCode then\n                let! body = response.Content.ReadAsStringAsync()\n                Assert.Fail($\"Expected add-summary success but got {(int response.StatusCode)} {response.StatusCode}: {body}\")\n                return Parameters.WorkItem.AddSummaryResult()\n            else\n                let! returnValue = deserializeContent<GraceReturnValue<Parameters.WorkItem.AddSummaryResult>> response\n                return returnValue.ReturnValue\n        }\n\n    let getArtifactDownloadUriAsync (client: HttpClient) (repositoryId: string) (artifactId: Guid) =\n        task {\n            let correlationId = generateCorrelationId ()\n\n            let route =\n                $\"/artifact/{artifactId}/download-uri?ownerId={ownerId}&organizationId={organizationId}&repositoryId={repositoryId}&correlationId={correlationId}\"\n\n            let! response = client.GetAsync(route)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<ArtifactDownloadUriResult>> response\n            return returnValue.ReturnValue.DownloadUri\n        }\n\n    let linkReferenceAsync (repositoryId: string) (workItemIdentifier: string) (referenceId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.LinkReferenceParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.ReferenceId <- referenceId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/work/link/reference\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n        }\n\n    let linkPromotionSetAsync (repositoryId: string) (workItemIdentifier: string) (promotionSetId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.LinkPromotionSetParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.PromotionSetId <- promotionSetId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/work/link/promotion-set\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n        }\n\n    let createArtifactAsync (repositoryId: string) (artifactType: string) =\n        task {\n            let parameters = Parameters.Artifact.CreateArtifactParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.ArtifactType <- artifactType\n            parameters.MimeType <- \"text/plain\"\n            parameters.Size <- 16L\n            parameters.Sha256 <- \"\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/artifact/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<ArtifactCreateResult>> response\n            return returnValue.ReturnValue.ArtifactId\n        }\n\n    let linkArtifactAsync (repositoryId: string) (workItemIdentifier: string) (artifactId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.LinkArtifactParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.ArtifactId <- artifactId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/work/link/artifact\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n        }\n\n    let removeReferenceLinkAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (referenceId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.RemoveReferenceLinkParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.ReferenceId <- referenceId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/links/remove/reference\", createJsonContent parameters)\n        }\n\n    let removePromotionSetLinkAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (promotionSetId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.RemovePromotionSetLinkParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.PromotionSetId <- promotionSetId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/links/remove/promotion-set\", createJsonContent parameters)\n        }\n\n    let removeArtifactLinkAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (artifactId: Guid) =\n        task {\n            let parameters = Parameters.WorkItem.RemoveArtifactLinkParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.ArtifactId <- artifactId.ToString()\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/links/remove/artifact\", createJsonContent parameters)\n        }\n\n    let removeArtifactTypeLinksAsync (client: HttpClient) (repositoryId: string) (workItemIdentifier: string) (artifactType: string) =\n        task {\n            let parameters = Parameters.WorkItem.RemoveArtifactTypeLinksParameters()\n            parameters.OwnerId <- ownerId\n            parameters.OrganizationId <- organizationId\n            parameters.RepositoryId <- repositoryId\n            parameters.WorkItemId <- workItemIdentifier\n            parameters.ArtifactType <- artifactType\n            parameters.CorrelationId <- generateCorrelationId ()\n            return! client.PostAsync(\"/work/links/remove/artifact-type\", createJsonContent parameters)\n        }\n\n    let createPersonalAccessTokenAsync () =\n        task {\n            let parameters = Parameters.Auth.CreatePersonalAccessTokenParameters()\n            parameters.TokenName <- $\"workitem-sdk-{Guid.NewGuid():N}\"\n            parameters.CorrelationId <- generateCorrelationId ()\n\n            let! response = Client.PostAsync(\"/auth/token/create\", createJsonContent parameters)\n            response.EnsureSuccessStatusCode() |> ignore\n            let! returnValue = deserializeContent<GraceReturnValue<PersonalAccessTokenCreated>> response\n            return returnValue.ReturnValue.Token\n        }\n\n    let configureSdkForServerAsync () =\n        task {\n            let configuration = Current()\n            configuration.ServerUri <- graceServerBaseAddress\n\n            let! token = createPersonalAccessTokenAsync ()\n\n            Grace.SDK.Auth.setTokenProvider (fun () -> task { return Some token })\n        }\n\n[<NonParallelizable>]\ntype WorkItemNumberAndLinksIntegrationTests() =\n\n    [<Test>]\n    member _.CreateThenFetchByGuidAndNumberReturnsSameWorkItem() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-id-number\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"id-number test\"\n            let! byId = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n            let! byNumber = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId (byId.WorkItemNumber.ToString())\n\n            Assert.That(byNumber.WorkItemId, Is.EqualTo(byId.WorkItemId))\n            Assert.That(byNumber.WorkItemNumber, Is.EqualTo(byId.WorkItemNumber))\n            Assert.That(byNumber.Title, Is.EqualTo(byId.Title))\n            Assert.That(byNumber.Description, Is.EqualTo(byId.Description))\n        }\n\n    [<Test>]\n    member _.UnknownNumericIdentifierReturnsNotFoundError() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-unknown-number\"\n            let! response = WorkItemIntegrationHelpers.getWorkItemResponseAsync Client repositoryId (Int64.MaxValue.ToString())\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n\n            Assert.That(error.Error, Does.Contain(WorkItemError.getErrorMessage WorkItemError.WorkItemDoesNotExist))\n        }\n\n    [<Test>]\n    member _.NonPositiveNumericIdentifierReturnsValidationError() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-invalid-number\"\n            let! response = WorkItemIntegrationHelpers.getWorkItemResponseAsync Client repositoryId \"0\"\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! error = deserializeContent<GraceError> response\n\n            Assert.That(error.Error, Does.Contain(WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemNumber))\n        }\n\n    [<Test>]\n    member _.SequentialCreatesProduceUniqueMonotonicNumbers() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-sequential\"\n            let createCount = 6\n            let workItemIds = ResizeArray<string>()\n\n            for index in 0 .. createCount - 1 do\n                let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId $\"sequential {index}\"\n                workItemIds.Add(workItemId)\n\n            let! numbers =\n                workItemIds.ToArray()\n                |> Array.map (fun workItemId ->\n                    task {\n                        let! workItem = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n                        return workItem.WorkItemNumber\n                    })\n                |> Task.WhenAll\n\n            Assert.That(numbers |> Array.distinct |> Array.length, Is.EqualTo(numbers.Length))\n\n            let strictlyIncreasing =\n                numbers\n                |> Array.pairwise\n                |> Array.forall (fun (first, second) -> second > first)\n\n            Assert.That(strictlyIncreasing, Is.True)\n        }\n\n    [<Test>]\n    member _.ConcurrentCreatesProduceUniqueNumbersWithoutCollisions() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-concurrent\"\n            let createCount = 24\n\n            let! workItemIds =\n                [|\n                    for index in 0 .. createCount - 1 -> WorkItemIntegrationHelpers.createWorkItemAsync repositoryId $\"concurrent {index}\"\n                |]\n                |> Task.WhenAll\n\n            let! numbers =\n                workItemIds\n                |> Array.map (fun workItemId ->\n                    task {\n                        let! workItem = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n                        return workItem.WorkItemNumber\n                    })\n                |> Task.WhenAll\n\n            Assert.That(numbers.Length, Is.EqualTo(createCount))\n            Assert.That(numbers |> Array.distinct |> Array.length, Is.EqualTo(createCount))\n            Assert.That(numbers |> Array.forall (fun value -> value > 0L), Is.True)\n        }\n\n    [<Test>]\n    member _.WorkItemLinksLifecycleRoundTripsAcrossReferencePromotionSetAndArtifacts() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-links-lifecycle\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"links lifecycle\"\n            let referenceId = Guid.NewGuid()\n            let promotionSetId = Guid.NewGuid()\n\n            do! WorkItemIntegrationHelpers.linkReferenceAsync repositoryId workItemId referenceId\n            do! WorkItemIntegrationHelpers.linkPromotionSetAsync repositoryId workItemId promotionSetId\n\n            let! linkedBeforeArtifacts = WorkItemIntegrationHelpers.getWorkItemLinksAsync Client repositoryId workItemId\n\n            Assert.That(linkedBeforeArtifacts.ReferenceIds, Has.Member(referenceId))\n            Assert.That(linkedBeforeArtifacts.PromotionSetIds, Has.Member(promotionSetId))\n\n            let summaryArtifactId = Guid.NewGuid()\n            let promptArtifactId = Guid.NewGuid()\n            let notesArtifactId = Guid.NewGuid()\n\n            do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId summaryArtifactId\n            do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId promptArtifactId\n            do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId notesArtifactId\n\n            let! removedReference = WorkItemIntegrationHelpers.removeReferenceLinkAsync Client repositoryId workItemId referenceId\n\n            removedReference.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! removedPromotionSet = WorkItemIntegrationHelpers.removePromotionSetLinkAsync Client repositoryId workItemId promotionSetId\n\n            removedPromotionSet.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! removedArtifact = WorkItemIntegrationHelpers.removeArtifactLinkAsync Client repositoryId workItemId summaryArtifactId\n\n            removedArtifact.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! removedSummary = WorkItemIntegrationHelpers.removeArtifactTypeLinksAsync Client repositoryId workItemId \"summary\"\n            removedSummary.EnsureSuccessStatusCode() |> ignore\n\n            let! removedPrompt = WorkItemIntegrationHelpers.removeArtifactTypeLinksAsync Client repositoryId workItemId \"prompt\"\n            removedPrompt.EnsureSuccessStatusCode() |> ignore\n\n            let! removedNotes = WorkItemIntegrationHelpers.removeArtifactTypeLinksAsync Client repositoryId workItemId \"notes\"\n            removedNotes.EnsureSuccessStatusCode() |> ignore\n\n            let! removedPromptArtifact = WorkItemIntegrationHelpers.removeArtifactLinkAsync Client repositoryId workItemId promptArtifactId\n\n            removedPromptArtifact.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! removedNotesArtifact = WorkItemIntegrationHelpers.removeArtifactLinkAsync Client repositoryId workItemId notesArtifactId\n\n            removedNotesArtifact.EnsureSuccessStatusCode()\n            |> ignore\n\n            let! afterRemoval = WorkItemIntegrationHelpers.getWorkItemLinksAsync Client repositoryId workItemId\n\n            Assert.That(afterRemoval.ReferenceIds, Is.Empty)\n            Assert.That(afterRemoval.PromotionSetIds, Is.Empty)\n            Assert.That(afterRemoval.AgentSummaryArtifactIds, Is.Empty)\n            Assert.That(afterRemoval.PromptArtifactIds, Is.Empty)\n            Assert.That(afterRemoval.ReviewNotesArtifactIds, Is.Empty)\n            Assert.That(afterRemoval.OtherArtifactIds, Is.Empty)\n            Assert.That(afterRemoval.ArtifactIds, Is.Empty)\n        }\n\n[<NonParallelizable>]\ntype WorkItemAddSummaryIntegrationTests() =\n\n    [<Test>]\n    member _.AddSummaryWithGuidCreatesSummaryLinkAndDownloadUri() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-add-summary-guid\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"add-summary guid\"\n            let summaryContent = $\"# Summary{Environment.NewLine}Guid flow replay validation\"\n\n            let! firstResult = WorkItemIntegrationHelpers.addSummaryAsync Client repositoryId workItemId summaryContent None None (generateCorrelationId ())\n\n            Assert.That(firstResult.WorkItemId, Is.EqualTo(workItemId))\n            Assert.That(firstResult.PromptArtifactId, Is.EqualTo(String.Empty))\n\n            let summaryArtifactId = Guid.Parse(firstResult.SummaryArtifactId)\n            let! workItemByGuid = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n            let! workItemByNumber = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId (workItemByGuid.WorkItemNumber.ToString())\n\n            Assert.That(workItemByGuid.ArtifactIds, Has.Member(summaryArtifactId))\n            Assert.That(workItemByNumber.ArtifactIds, Has.Member(summaryArtifactId))\n\n            let summaryArtifactCountByGuid =\n                workItemByGuid.ArtifactIds\n                |> List.filter (fun artifactId -> artifactId = summaryArtifactId)\n                |> List.length\n\n            Assert.That(summaryArtifactCountByGuid, Is.EqualTo(1))\n\n            let! summaryDownloadUri = WorkItemIntegrationHelpers.getArtifactDownloadUriAsync Client repositoryId summaryArtifactId\n            Assert.That(summaryDownloadUri, Is.Not.Null)\n            Assert.That(String.IsNullOrWhiteSpace(summaryDownloadUri.AbsoluteUri), Is.False)\n        }\n\n    [<Test>]\n    member _.AddSummaryWithWorkItemNumberRoundTripsPromptAndLinksAcrossBothIdentifiers() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-add-summary-number\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"add-summary number\"\n            let! workItem = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n            let workItemNumber = workItem.WorkItemNumber.ToString()\n            let summaryContent = $\"# Summary{Environment.NewLine}Number flow\"\n            let promptContent = $\"# Prompt{Environment.NewLine}Number flow\"\n\n            let! addSummaryResult =\n                WorkItemIntegrationHelpers.addSummaryAsync\n                    Client\n                    repositoryId\n                    workItemNumber\n                    summaryContent\n                    (Some promptContent)\n                    (Some \"agent://codex\")\n                    (generateCorrelationId ())\n\n            let summaryArtifactId = Guid.Parse(addSummaryResult.SummaryArtifactId)\n            let promptArtifactId = Guid.Parse(addSummaryResult.PromptArtifactId)\n\n            let! workItemByNumber = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemNumber\n            let! workItemByGuid = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n\n            Assert.That(workItemByNumber.WorkItemId, Is.EqualTo(Guid.Parse(workItemId)))\n            Assert.That(workItemByGuid.WorkItemId, Is.EqualTo(Guid.Parse(workItemId)))\n            Assert.That(workItemByNumber.WorkItemNumber, Is.EqualTo(workItem.WorkItemNumber))\n            Assert.That(workItemByGuid.WorkItemNumber, Is.EqualTo(workItem.WorkItemNumber))\n\n            Assert.That(workItemByNumber.ArtifactIds, Has.Member(summaryArtifactId))\n            Assert.That(workItemByNumber.ArtifactIds, Has.Member(promptArtifactId))\n            Assert.That(workItemByGuid.ArtifactIds, Has.Member(summaryArtifactId))\n            Assert.That(workItemByGuid.ArtifactIds, Has.Member(promptArtifactId))\n\n            let! summaryDownloadUri = WorkItemIntegrationHelpers.getArtifactDownloadUriAsync Client repositoryId summaryArtifactId\n            let! promptDownloadUri = WorkItemIntegrationHelpers.getArtifactDownloadUriAsync Client repositoryId promptArtifactId\n\n            Assert.That(summaryDownloadUri, Is.Not.Null)\n            Assert.That(promptDownloadUri, Is.Not.Null)\n            Assert.That(String.IsNullOrWhiteSpace(summaryDownloadUri.AbsoluteUri), Is.False)\n            Assert.That(String.IsNullOrWhiteSpace(promptDownloadUri.AbsoluteUri), Is.False)\n        }\n\n    [<Test>]\n    member _.AddSummaryRejectsCallerSuppliedArtifactIdsForNumericIdentifiers() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-add-summary-reject-artifact-id\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"reject caller supplied artifact id\"\n            let! workItem = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n            let workItemNumber = workItem.WorkItemNumber.ToString()\n\n            let! response =\n                WorkItemIntegrationHelpers.addSummaryResponseAsync\n                    Client\n                    repositoryId\n                    workItemNumber\n                    \"summary content\"\n                    None\n                    None\n                    (Some(Guid.NewGuid().ToString()))\n                    (generateCorrelationId ())\n\n            Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! graceError = deserializeContent<GraceError> response\n\n            Assert.That(graceError.Error, Does.Contain(\"Caller-supplied artifact IDs are not supported by add-summary\"))\n            Assert.That(graceError.Error, Does.Contain(\"Canonical add-summary requests must provide SummaryContent\"))\n        }\n\n[<NonParallelizable>]\ntype WorkItemAttachmentEndpointsIntegrationTests() =\n\n    [<Test>]\n    member _.AttachmentListSupportsGuidAndNumberAndFiltersToReviewerAttachmentTypes() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-attachments-list\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"attachments list\"\n\n            let! addSummaryResult =\n                WorkItemIntegrationHelpers.addSummaryAsync\n                    Client\n                    repositoryId\n                    workItemId\n                    \"summary list content\"\n                    (Some \"prompt list content\")\n                    None\n                    (generateCorrelationId ())\n\n            let! otherArtifactId = WorkItemIntegrationHelpers.createArtifactAsync repositoryId \"Other\"\n            do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId otherArtifactId\n\n            let! workItemByGuid = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n            let workItemNumber = workItemByGuid.WorkItemNumber.ToString()\n\n            let! attachmentsByGuid = WorkItemIntegrationHelpers.listWorkItemAttachmentsAsync Client repositoryId workItemId\n            let! attachmentsByNumber = WorkItemIntegrationHelpers.listWorkItemAttachmentsAsync Client repositoryId workItemNumber\n\n            Assert.That(attachmentsByGuid.WorkItemId, Is.EqualTo(workItemId))\n            Assert.That(attachmentsByGuid.WorkItemNumber, Is.EqualTo(workItemByGuid.WorkItemNumber))\n            Assert.That(attachmentsByNumber.WorkItemId, Is.EqualTo(workItemId))\n            Assert.That(attachmentsByNumber.WorkItemNumber, Is.EqualTo(workItemByGuid.WorkItemNumber))\n\n            let attachmentIdsByGuid =\n                attachmentsByGuid.Attachments\n                |> Seq.map (fun attachment -> attachment.ArtifactId)\n                |> Seq.toArray\n\n            let attachmentIdsByNumber =\n                attachmentsByNumber.Attachments\n                |> Seq.map (fun attachment -> attachment.ArtifactId)\n                |> Seq.toArray\n\n            Assert.That(attachmentIdsByGuid, Is.EquivalentTo(attachmentIdsByNumber))\n            Assert.That(attachmentIdsByGuid, Has.Member(addSummaryResult.SummaryArtifactId))\n            Assert.That(attachmentIdsByGuid.Length, Is.GreaterThanOrEqualTo(1))\n        }\n\n    [<Test>]\n    member _.AttachmentShowSelectsDeterministicLatestOrEarliestByAttachmentType() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-attachments-show\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"attachments show\"\n            let firstSummaryContent = \"first summary content\"\n            let secondSummaryContent = \"second summary content\"\n\n            let! firstResult =\n                WorkItemIntegrationHelpers.addSummaryAsync Client repositoryId workItemId firstSummaryContent None None (generateCorrelationId ())\n\n            do! Task.Delay(20)\n\n            let! secondResult =\n                WorkItemIntegrationHelpers.addSummaryAsync Client repositoryId workItemId secondSummaryContent None None (generateCorrelationId ())\n\n            let! workItemByGuid = WorkItemIntegrationHelpers.getWorkItemDtoAsync Client repositoryId workItemId\n            let workItemNumber = workItemByGuid.WorkItemNumber.ToString()\n            let! attachmentList = WorkItemIntegrationHelpers.listWorkItemAttachmentsAsync Client repositoryId workItemId\n\n            let orderedAttachmentIds =\n                attachmentList.Attachments\n                |> Seq.map (fun attachment -> attachment.ArtifactId)\n                |> Seq.toArray\n\n            let! showEarliest = WorkItemIntegrationHelpers.showWorkItemAttachmentAsync Client repositoryId workItemId \"summary\" false\n\n            let! showLatest = WorkItemIntegrationHelpers.showWorkItemAttachmentAsync Client repositoryId workItemNumber \"summary\" true\n            let! showLatestAgain = WorkItemIntegrationHelpers.showWorkItemAttachmentAsync Client repositoryId workItemNumber \"summary\" true\n\n            Assert.That(orderedAttachmentIds.Length, Is.GreaterThanOrEqualTo(1))\n            Assert.That(String.IsNullOrWhiteSpace(showEarliest.ArtifactId), Is.False)\n            Assert.That(showEarliest.SelectedUsingLatest, Is.False)\n            Assert.That(showEarliest.AvailableAttachmentCount, Is.GreaterThanOrEqualTo(1))\n\n            Assert.That(showLatest.ArtifactId, Is.EqualTo(showLatestAgain.ArtifactId))\n            Assert.That(showLatest.SelectedUsingLatest, Is.True)\n            Assert.That(showLatest.AvailableAttachmentCount, Is.GreaterThanOrEqualTo(1))\n        }\n\n    [<Test>]\n    member _.AttachmentDownloadReturnsDownloadUriForLinkedReviewerAttachmentAndRejectsInvalidArtifacts() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-attachments-download\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"attachments download\"\n\n            let! addSummaryResult =\n                WorkItemIntegrationHelpers.addSummaryAsync Client repositoryId workItemId \"download summary content\" None None (generateCorrelationId ())\n\n            let! linkedOtherArtifactId = WorkItemIntegrationHelpers.createArtifactAsync repositoryId \"Other\"\n            do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId linkedOtherArtifactId\n\n            let summaryArtifactId = Guid.Parse(addSummaryResult.SummaryArtifactId)\n            let! downloadResult = WorkItemIntegrationHelpers.downloadWorkItemAttachmentAsync Client repositoryId workItemId summaryArtifactId\n\n            Assert.That(downloadResult.ArtifactId, Is.EqualTo(summaryArtifactId.ToString()))\n            Assert.That(downloadResult.AttachmentType, Is.EqualTo(\"summary\"))\n            Assert.That(String.IsNullOrWhiteSpace(downloadResult.DownloadUri), Is.False)\n\n            let! notLinkedResponse = WorkItemIntegrationHelpers.downloadWorkItemAttachmentResponseAsync Client repositoryId workItemId (Guid.NewGuid())\n\n            Assert.That(notLinkedResponse.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n            let! notLinkedError = deserializeContent<GraceError> notLinkedResponse\n            Assert.That(notLinkedError.Error, Does.Contain(\"not linked\"))\n        }\n\n[<Parallelizable(ParallelScope.All)>]\ntype WorkItemLinksAuthorizationIntegrationTests() =\n\n    [<Test>]\n    member _.WorkItemLinkEndpointsRequireAuthenticationAndAllowAuthenticatedUsers() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-links-auth\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"auth matrix\"\n\n            let summaryArtifactId = Guid.NewGuid()\n            use unauthenticatedClient = WorkItemIntegrationHelpers.createUnauthenticatedClient ()\n            use authenticatedClient = WorkItemIntegrationHelpers.createAuthenticatedClient $\"{Guid.NewGuid()}\"\n\n            let calls: (string * (unit -> Task<HttpResponseMessage>)) list =\n                [\n                    \"/work/links/list\",\n                    (fun () ->\n                        task {\n                            let parameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n                            parameters.OwnerId <- ownerId\n                            parameters.OrganizationId <- organizationId\n                            parameters.RepositoryId <- repositoryId\n                            parameters.WorkItemId <- workItemId\n                            parameters.CorrelationId <- generateCorrelationId ()\n                            return! unauthenticatedClient.PostAsync(\"/work/links/list\", createJsonContent parameters)\n                        })\n                    \"/work/attachments/list\",\n                    (fun () ->\n                        task {\n                            let parameters = Parameters.WorkItem.ListWorkItemAttachmentsParameters()\n                            parameters.OwnerId <- ownerId\n                            parameters.OrganizationId <- organizationId\n                            parameters.RepositoryId <- repositoryId\n                            parameters.WorkItemId <- workItemId\n                            parameters.CorrelationId <- generateCorrelationId ()\n                            return! unauthenticatedClient.PostAsync(\"/work/attachments/list\", createJsonContent parameters)\n                        })\n                    \"/work/attachments/show\",\n                    (fun () ->\n                        task {\n                            let parameters = Parameters.WorkItem.ShowWorkItemAttachmentParameters()\n                            parameters.OwnerId <- ownerId\n                            parameters.OrganizationId <- organizationId\n                            parameters.RepositoryId <- repositoryId\n                            parameters.WorkItemId <- workItemId\n                            parameters.AttachmentType <- \"summary\"\n                            parameters.Latest <- true\n                            parameters.CorrelationId <- generateCorrelationId ()\n                            return! unauthenticatedClient.PostAsync(\"/work/attachments/show\", createJsonContent parameters)\n                        })\n                    \"/work/attachments/download\",\n                    (fun () ->\n                        task {\n                            let parameters = Parameters.WorkItem.DownloadWorkItemAttachmentParameters()\n                            parameters.OwnerId <- ownerId\n                            parameters.OrganizationId <- organizationId\n                            parameters.RepositoryId <- repositoryId\n                            parameters.WorkItemId <- workItemId\n                            parameters.ArtifactId <- summaryArtifactId.ToString()\n                            parameters.CorrelationId <- generateCorrelationId ()\n                            return! unauthenticatedClient.PostAsync(\"/work/attachments/download\", createJsonContent parameters)\n                        })\n                    \"/work/links/remove/reference\",\n                    (fun () -> WorkItemIntegrationHelpers.removeReferenceLinkAsync unauthenticatedClient repositoryId workItemId (Guid.NewGuid()))\n                    \"/work/links/remove/promotion-set\",\n                    (fun () -> WorkItemIntegrationHelpers.removePromotionSetLinkAsync unauthenticatedClient repositoryId workItemId (Guid.NewGuid()))\n                    \"/work/links/remove/artifact\",\n                    (fun () -> WorkItemIntegrationHelpers.removeArtifactLinkAsync unauthenticatedClient repositoryId workItemId (Guid.NewGuid()))\n                    \"/work/links/remove/artifact-type\",\n                    (fun () -> WorkItemIntegrationHelpers.removeArtifactTypeLinksAsync unauthenticatedClient repositoryId workItemId \"summary\")\n                ]\n\n            for (path, invokeUnauthenticated) in calls do\n                let! response = invokeUnauthenticated ()\n                Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized), $\"Expected 401 for {path}.\")\n\n            let! listAuthenticated =\n                task {\n                    let parameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n                    parameters.OwnerId <- ownerId\n                    parameters.OrganizationId <- organizationId\n                    parameters.RepositoryId <- repositoryId\n                    parameters.WorkItemId <- workItemId\n                    parameters.CorrelationId <- generateCorrelationId ()\n                    return! authenticatedClient.PostAsync(\"/work/links/list\", createJsonContent parameters)\n                }\n\n            Assert.That(listAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! listAttachmentsAuthenticated = WorkItemIntegrationHelpers.listWorkItemAttachmentsResponseAsync authenticatedClient repositoryId workItemId\n\n            Assert.That(listAttachmentsAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! showAttachmentAuthenticated =\n                WorkItemIntegrationHelpers.showWorkItemAttachmentResponseAsync authenticatedClient repositoryId workItemId \"summary\" true\n\n            Assert.That(showAttachmentAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n\n            let! downloadAttachmentAuthenticated =\n                WorkItemIntegrationHelpers.downloadWorkItemAttachmentResponseAsync authenticatedClient repositoryId workItemId summaryArtifactId\n\n            Assert.That(downloadAttachmentAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest))\n\n            let! removeReferenceAuthenticated = WorkItemIntegrationHelpers.removeReferenceLinkAsync authenticatedClient repositoryId workItemId (Guid.NewGuid())\n\n            Assert.That(removeReferenceAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! removePromotionAuthenticated =\n                WorkItemIntegrationHelpers.removePromotionSetLinkAsync authenticatedClient repositoryId workItemId (Guid.NewGuid())\n\n            Assert.That(removePromotionAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! removeArtifactAuthenticated = WorkItemIntegrationHelpers.removeArtifactLinkAsync authenticatedClient repositoryId workItemId (Guid.NewGuid())\n\n            Assert.That(removeArtifactAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n\n            let! removeArtifactTypeAuthenticated = WorkItemIntegrationHelpers.removeArtifactTypeLinksAsync authenticatedClient repositoryId workItemId \"summary\"\n\n            Assert.That(removeArtifactTypeAuthenticated.StatusCode, Is.EqualTo(HttpStatusCode.OK))\n        }\n\n[<NonParallelizable>]\ntype WorkItemSdkSmokeIntegrationTests() =\n\n    let runWithSdkAuthentication (testBody: unit -> Task<unit>) =\n        task {\n            do! WorkItemIntegrationHelpers.configureSdkForServerAsync ()\n\n            try\n                do! testBody ()\n            finally\n                Grace.SDK.Auth.clearTokenProvider ()\n        }\n\n    [<Test>]\n    member _.SdkWorkItemLinkApisRoundTrip() =\n        task {\n            do!\n                runWithSdkAuthentication (fun () ->\n                    task {\n                        let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-sdk-roundtrip\"\n                        let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"sdk links\"\n                        let referenceId = Guid.NewGuid()\n                        let promotionSetId = Guid.NewGuid()\n\n                        do! WorkItemIntegrationHelpers.linkReferenceAsync repositoryId workItemId referenceId\n                        do! WorkItemIntegrationHelpers.linkPromotionSetAsync repositoryId workItemId promotionSetId\n\n                        let linksParameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n                        linksParameters.OwnerId <- ownerId\n                        linksParameters.OrganizationId <- organizationId\n                        linksParameters.RepositoryId <- repositoryId\n                        linksParameters.WorkItemId <- workItemId\n                        linksParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! linksResult = Grace.SDK.WorkItem.GetLinks linksParameters\n\n                        let linked =\n                            match linksResult with\n                            | Ok returnValue -> returnValue.ReturnValue\n                            | Error error ->\n                                Assert.Fail($\"Expected SDK GetLinks success but got error: {error.Error}\")\n                                WorkItemLinksDto.Default\n\n                        Assert.That(linked.ReferenceIds, Has.Member(referenceId))\n                        Assert.That(linked.PromotionSetIds, Has.Member(promotionSetId))\n\n                        let summaryArtifactId = Guid.NewGuid()\n                        let promptArtifactId = Guid.NewGuid()\n                        let notesArtifactId = Guid.NewGuid()\n\n                        do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId summaryArtifactId\n                        do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId promptArtifactId\n                        do! WorkItemIntegrationHelpers.linkArtifactAsync repositoryId workItemId notesArtifactId\n\n                        let removeReferenceParameters = Parameters.WorkItem.RemoveReferenceLinkParameters()\n                        removeReferenceParameters.OwnerId <- ownerId\n                        removeReferenceParameters.OrganizationId <- organizationId\n                        removeReferenceParameters.RepositoryId <- repositoryId\n                        removeReferenceParameters.WorkItemId <- workItemId\n                        removeReferenceParameters.ReferenceId <- referenceId.ToString()\n                        removeReferenceParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removeReferenceResult = Grace.SDK.WorkItem.RemoveReferenceLink removeReferenceParameters\n                        Assert.That(removeReferenceResult.IsOk, Is.True)\n\n                        let removePromotionParameters = Parameters.WorkItem.RemovePromotionSetLinkParameters()\n                        removePromotionParameters.OwnerId <- ownerId\n                        removePromotionParameters.OrganizationId <- organizationId\n                        removePromotionParameters.RepositoryId <- repositoryId\n                        removePromotionParameters.WorkItemId <- workItemId\n                        removePromotionParameters.PromotionSetId <- promotionSetId.ToString()\n                        removePromotionParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removePromotionResult = Grace.SDK.WorkItem.RemovePromotionSetLink removePromotionParameters\n                        Assert.That(removePromotionResult.IsOk, Is.True)\n\n                        let removeArtifactParameters = Parameters.WorkItem.RemoveArtifactLinkParameters()\n                        removeArtifactParameters.OwnerId <- ownerId\n                        removeArtifactParameters.OrganizationId <- organizationId\n                        removeArtifactParameters.RepositoryId <- repositoryId\n                        removeArtifactParameters.WorkItemId <- workItemId\n                        removeArtifactParameters.ArtifactId <- summaryArtifactId.ToString()\n                        removeArtifactParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removeArtifactResult = Grace.SDK.WorkItem.RemoveArtifactLink removeArtifactParameters\n                        Assert.That(removeArtifactResult.IsOk, Is.True)\n\n                        let removeSummaryParameters = Parameters.WorkItem.RemoveArtifactTypeLinksParameters()\n                        removeSummaryParameters.OwnerId <- ownerId\n                        removeSummaryParameters.OrganizationId <- organizationId\n                        removeSummaryParameters.RepositoryId <- repositoryId\n                        removeSummaryParameters.WorkItemId <- workItemId\n                        removeSummaryParameters.ArtifactType <- \"summary\"\n                        removeSummaryParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removeSummaryResult = Grace.SDK.WorkItem.RemoveArtifactTypeLinks removeSummaryParameters\n                        Assert.That(removeSummaryResult.IsOk, Is.True)\n\n                        let removePromptParameters = Parameters.WorkItem.RemoveArtifactTypeLinksParameters()\n                        removePromptParameters.OwnerId <- ownerId\n                        removePromptParameters.OrganizationId <- organizationId\n                        removePromptParameters.RepositoryId <- repositoryId\n                        removePromptParameters.WorkItemId <- workItemId\n                        removePromptParameters.ArtifactType <- \"prompt\"\n                        removePromptParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removePromptResult = Grace.SDK.WorkItem.RemoveArtifactTypeLinks removePromptParameters\n                        Assert.That(removePromptResult.IsOk, Is.True)\n\n                        let removeNotesParameters = Parameters.WorkItem.RemoveArtifactTypeLinksParameters()\n                        removeNotesParameters.OwnerId <- ownerId\n                        removeNotesParameters.OrganizationId <- organizationId\n                        removeNotesParameters.RepositoryId <- repositoryId\n                        removeNotesParameters.WorkItemId <- workItemId\n                        removeNotesParameters.ArtifactType <- \"notes\"\n                        removeNotesParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removeNotesResult = Grace.SDK.WorkItem.RemoveArtifactTypeLinks removeNotesParameters\n                        Assert.That(removeNotesResult.IsOk, Is.True)\n\n                        let removePromptArtifactParameters = Parameters.WorkItem.RemoveArtifactLinkParameters()\n                        removePromptArtifactParameters.OwnerId <- ownerId\n                        removePromptArtifactParameters.OrganizationId <- organizationId\n                        removePromptArtifactParameters.RepositoryId <- repositoryId\n                        removePromptArtifactParameters.WorkItemId <- workItemId\n                        removePromptArtifactParameters.ArtifactId <- promptArtifactId.ToString()\n                        removePromptArtifactParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removePromptArtifactResult = Grace.SDK.WorkItem.RemoveArtifactLink removePromptArtifactParameters\n                        Assert.That(removePromptArtifactResult.IsOk, Is.True)\n\n                        let removeNotesArtifactParameters = Parameters.WorkItem.RemoveArtifactLinkParameters()\n                        removeNotesArtifactParameters.OwnerId <- ownerId\n                        removeNotesArtifactParameters.OrganizationId <- organizationId\n                        removeNotesArtifactParameters.RepositoryId <- repositoryId\n                        removeNotesArtifactParameters.WorkItemId <- workItemId\n                        removeNotesArtifactParameters.ArtifactId <- notesArtifactId.ToString()\n                        removeNotesArtifactParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! removeNotesArtifactResult = Grace.SDK.WorkItem.RemoveArtifactLink removeNotesArtifactParameters\n                        Assert.That(removeNotesArtifactResult.IsOk, Is.True)\n\n                        let! linksAfterResult = Grace.SDK.WorkItem.GetLinks linksParameters\n\n                        let afterRemoval =\n                            match linksAfterResult with\n                            | Ok returnValue -> returnValue.ReturnValue\n                            | Error error ->\n                                Assert.Fail($\"Expected SDK GetLinks success but got error after removals: {error.Error}\")\n                                WorkItemLinksDto.Default\n\n                        Assert.That(afterRemoval.ReferenceIds, Is.Empty)\n                        Assert.That(afterRemoval.PromotionSetIds, Is.Empty)\n                        Assert.That(afterRemoval.AgentSummaryArtifactIds, Is.Empty)\n                        Assert.That(afterRemoval.PromptArtifactIds, Is.Empty)\n                        Assert.That(afterRemoval.ReviewNotesArtifactIds, Is.Empty)\n                        Assert.That(afterRemoval.OtherArtifactIds, Is.Empty)\n                    })\n        }\n\n    [<Test>]\n    member _.SdkWorkItemAttachmentApisSupportListShowAndDownload() =\n        task {\n            do!\n                runWithSdkAuthentication (fun () ->\n                    task {\n                        let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-sdk-attachments\"\n                        let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"sdk attachments\"\n\n                        let firstSummaryContent = \"sdk summary one\"\n                        let secondSummaryContent = \"sdk summary two\"\n\n                        let! firstSummary =\n                            WorkItemIntegrationHelpers.addSummaryAsync Client repositoryId workItemId firstSummaryContent None None (generateCorrelationId ())\n\n                        do! Task.Delay(20)\n\n                        let! secondSummary =\n                            WorkItemIntegrationHelpers.addSummaryAsync Client repositoryId workItemId secondSummaryContent None None (generateCorrelationId ())\n\n                        let listParameters = Parameters.WorkItem.ListWorkItemAttachmentsParameters()\n                        listParameters.OwnerId <- ownerId\n                        listParameters.OrganizationId <- organizationId\n                        listParameters.RepositoryId <- repositoryId\n                        listParameters.WorkItemId <- workItemId\n                        listParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! listResult = Grace.SDK.WorkItem.ListAttachments listParameters\n\n                        let attachments =\n                            match listResult with\n                            | Ok returnValue -> returnValue.ReturnValue.Attachments |> Seq.toArray\n                            | Error error ->\n                                Assert.Fail($\"Expected SDK ListAttachments success but got error: {error.Error}\")\n                                Array.empty\n\n                        Assert.That(\n                            attachments\n                            |> Array.map (fun attachment -> attachment.ArtifactId),\n                            Has.Member(firstSummary.SummaryArtifactId)\n                        )\n\n                        Assert.That(\n                            attachments\n                            |> Array.map (fun attachment -> attachment.ArtifactId),\n                            Has.Member(secondSummary.SummaryArtifactId)\n                        )\n\n                        let orderedAttachmentIds =\n                            attachments\n                            |> Array.map (fun attachment -> attachment.ArtifactId)\n\n                        Assert.That(orderedAttachmentIds.Length, Is.GreaterThanOrEqualTo(1))\n\n                        let showParameters = Parameters.WorkItem.ShowWorkItemAttachmentParameters()\n                        showParameters.OwnerId <- ownerId\n                        showParameters.OrganizationId <- organizationId\n                        showParameters.RepositoryId <- repositoryId\n                        showParameters.WorkItemId <- workItemId\n                        showParameters.AttachmentType <- \"summary\"\n                        showParameters.Latest <- true\n                        showParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! showResult = Grace.SDK.WorkItem.ShowAttachment showParameters\n\n                        let shownAttachment =\n                            match showResult with\n                            | Ok returnValue -> returnValue.ReturnValue\n                            | Error error ->\n                                Assert.Fail($\"Expected SDK ShowAttachment success but got error: {error.Error}\")\n                                Parameters.WorkItem.ShowWorkItemAttachmentResult()\n\n                        Assert.That(String.IsNullOrWhiteSpace(shownAttachment.ArtifactId), Is.False)\n\n                        let downloadParameters = Parameters.WorkItem.DownloadWorkItemAttachmentParameters()\n                        downloadParameters.OwnerId <- ownerId\n                        downloadParameters.OrganizationId <- organizationId\n                        downloadParameters.RepositoryId <- repositoryId\n                        downloadParameters.WorkItemId <- workItemId\n                        downloadParameters.ArtifactId <- shownAttachment.ArtifactId\n                        downloadParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! downloadResult = Grace.SDK.WorkItem.DownloadAttachment downloadParameters\n\n                        let download =\n                            match downloadResult with\n                            | Ok returnValue -> returnValue.ReturnValue\n                            | Error error ->\n                                Assert.Fail($\"Expected SDK DownloadAttachment success but got error: {error.Error}\")\n                                Parameters.WorkItem.DownloadWorkItemAttachmentResult()\n\n                        Assert.That(download.ArtifactId, Is.EqualTo(shownAttachment.ArtifactId))\n                        Assert.That(download.AttachmentType, Is.EqualTo(\"summary\"))\n                        Assert.That(String.IsNullOrWhiteSpace(download.DownloadUri), Is.False)\n                    })\n        }\n\n    [<Test>]\n    member _.SdkWorkItemLinkApisPropagateValidationNotFoundAndAuthorizationErrors() =\n        task {\n            let! repositoryId = WorkItemIntegrationHelpers.createRepositoryAsync \"wi-sdk-errors\"\n            let! workItemId = WorkItemIntegrationHelpers.createWorkItemAsync repositoryId \"sdk errors\"\n\n            do!\n                runWithSdkAuthentication (fun () ->\n                    task {\n                        let invalidParameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n                        invalidParameters.OwnerId <- ownerId\n                        invalidParameters.OrganizationId <- organizationId\n                        invalidParameters.RepositoryId <- repositoryId\n                        invalidParameters.WorkItemId <- \"0\"\n                        invalidParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! invalidResult = Grace.SDK.WorkItem.GetLinks invalidParameters\n\n                        match invalidResult with\n                        | Ok _ -> Assert.Fail(\"Expected validation error for non-positive WorkItemNumber.\")\n                        | Error error -> Assert.That(error.Error, Does.Contain(WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemNumber))\n\n                        let notFoundParameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n                        notFoundParameters.OwnerId <- ownerId\n                        notFoundParameters.OrganizationId <- organizationId\n                        notFoundParameters.RepositoryId <- repositoryId\n                        notFoundParameters.WorkItemId <- Int64.MaxValue.ToString()\n                        notFoundParameters.CorrelationId <- generateCorrelationId ()\n\n                        let! notFoundResult = Grace.SDK.WorkItem.GetLinks notFoundParameters\n\n                        match notFoundResult with\n                        | Ok _ -> Assert.Fail(\"Expected not-found error for unknown WorkItemNumber.\")\n                        | Error error -> Assert.That(error.Error, Does.Contain(WorkItemError.getErrorMessage WorkItemError.WorkItemDoesNotExist))\n                    })\n\n            let configuration = Current()\n            configuration.ServerUri <- graceServerBaseAddress\n            Grace.SDK.Auth.clearTokenProvider ()\n\n            let unauthorizedParameters = Parameters.WorkItem.GetWorkItemLinksParameters()\n            unauthorizedParameters.OwnerId <- ownerId\n            unauthorizedParameters.OrganizationId <- organizationId\n            unauthorizedParameters.RepositoryId <- repositoryId\n            unauthorizedParameters.WorkItemId <- workItemId\n            unauthorizedParameters.CorrelationId <- generateCorrelationId ()\n\n            let! unauthorizedResult = Grace.SDK.WorkItem.GetLinks unauthorizedParameters\n\n            match unauthorizedResult with\n            | Ok _ -> Assert.Fail(\"Expected authorization error when SDK token provider is not configured.\")\n            | Error _ -> ()\n        }\n"
  },
  {
    "path": "src/Grace.Server.Tests/WorkItem.Server.Tests.fs",
    "content": "namespace Grace.Server.Tests\n\nopen FsCheck\nopen FsCheck.NUnit\nopen Grace.Server\nopen Grace.Shared.Validation.Utilities\nopen Grace.Types.Artifact\nopen Grace.Shared.Parameters.WorkItem\nopen Grace.Shared.Validation.Errors\nopen Grace.Types.Types\nopen Grace.Types.WorkItem\nopen NUnit.Framework\nopen NodaTime\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype WorkItemServerUnitTests() =\n    let metadata timestamp = { Timestamp = timestamp; CorrelationId = \"corr-work-item\"; Principal = \"tester\"; Properties = Dictionary<string, string>() }\n\n    let runValidation (validation: Threading.Tasks.ValueTask<Result<unit, WorkItemError>>) =\n        validation.AsTask()\n        |> Async.AwaitTask\n        |> Async.RunSynchronously\n\n    let applyCasePattern (pattern: bool array) (value: string) =\n        let toggles = if isNull pattern then [||] else pattern\n\n        value\n        |> Seq.mapi (fun index character ->\n            if Char.IsLetter character then\n                let useUpper = if toggles.Length = 0 then false else toggles[index % toggles.Length]\n\n                if useUpper then\n                    Char.ToUpperInvariant character\n                else\n                    Char.ToLowerInvariant character\n            else\n                character)\n        |> Seq.toArray\n        |> String\n\n    [<Test>]\n    member _.UpdateCommandsEmptyWhenNoFieldsProvided() =\n        let parameters = UpdateWorkItemParameters(WorkItemId = Guid.NewGuid().ToString())\n        let commands = WorkItem.buildUpdateCommands parameters\n        Assert.That(commands, Is.Empty)\n\n    [<Test>]\n    member _.UpdateCommandsOrderedForMultipleFields() =\n        let parameters =\n            UpdateWorkItemParameters(\n                WorkItemId = Guid.NewGuid().ToString(),\n                Title = \"Title\",\n                Description = \"Description\",\n                Status = WorkItemStatus.Active.ToString(),\n                Constraints = \"Constraints\",\n                Notes = \"Notes\",\n                ArchitecturalNotes = \"Architecture\",\n                MigrationNotes = \"Migration\"\n            )\n\n        let commands = WorkItem.buildUpdateCommands parameters\n\n        let expected: WorkItemCommand list =\n            [\n                WorkItemCommand.SetTitle \"Title\"\n                WorkItemCommand.SetDescription \"Description\"\n                WorkItemCommand.SetStatus WorkItemStatus.Active\n                WorkItemCommand.SetConstraints \"Constraints\"\n                WorkItemCommand.SetNotes \"Notes\"\n                WorkItemCommand.SetArchitecturalNotes \"Architecture\"\n                WorkItemCommand.SetMigrationNotes \"Migration\"\n            ]\n\n        let matches = commands = expected\n        Assert.That(matches, Is.True)\n\n    [<Test>]\n    member _.LinkReferenceValidationRejectsInvalidReferenceId() =\n        let parameters = LinkReferenceParameters(WorkItemId = Guid.NewGuid().ToString(), ReferenceId = \"not-a-guid\")\n\n        let validations = WorkItem.validateLinkReferenceParameters parameters\n\n        let error =\n            validations\n            |> getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(error, Is.EqualTo(Some WorkItemError.InvalidReferenceId))\n\n    [<Test>]\n    member _.WorkItemIdentifierValidationAcceptsPositiveNumber() =\n        let result =\n            WorkItem.validateWorkItemIdentifier \"123\"\n            |> runValidation\n\n        Assert.That(result, Is.EqualTo(Ok(): Result<unit, WorkItemError>))\n\n    [<Test>]\n    member _.WorkItemIdentifierValidationRejectsNonPositiveNumber() =\n        let result =\n            WorkItem.validateWorkItemIdentifier \"0\"\n            |> runValidation\n\n        Assert.That(result, Is.EqualTo(Error WorkItemError.InvalidWorkItemNumber: Result<unit, WorkItemError>))\n\n    [<Test>]\n    member _.WorkItemIdentifierValidationAcceptsGuid() =\n        let workItemId = Guid.NewGuid().ToString()\n\n        let result =\n            WorkItem.validateWorkItemIdentifier workItemId\n            |> runValidation\n\n        Assert.That(result, Is.EqualTo(Ok(): Result<unit, WorkItemError>))\n\n    [<Test>]\n    member _.WorkItemIdentifierValidationRejectsInvalidIdentifier() =\n        let result =\n            WorkItem.validateWorkItemIdentifier \"not-a-guid-or-number\"\n            |> runValidation\n\n        Assert.That(result, Is.EqualTo(Error WorkItemError.InvalidWorkItemId: Result<unit, WorkItemError>))\n\n    [<Test>]\n    member _.LinkPromotionSetValidationRejectsInvalidPromotionSetId() =\n        let parameters = LinkPromotionSetParameters(WorkItemId = Guid.NewGuid().ToString(), PromotionSetId = \"not-a-guid\")\n\n        let validations = WorkItem.validateLinkPromotionSetParameters parameters\n\n        let error =\n            validations\n            |> getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(error, Is.EqualTo(Some WorkItemError.InvalidPromotionSetId))\n\n    [<Test>]\n    member _.LinkArtifactValidationRejectsInvalidArtifactId() =\n        let parameters = LinkArtifactParameters(WorkItemId = Guid.NewGuid().ToString(), ArtifactId = \"not-a-guid\")\n\n        let validations = WorkItem.validateLinkArtifactParameters parameters\n\n        let error =\n            validations\n            |> getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(error, Is.EqualTo(Some WorkItemError.InvalidArtifactId))\n\n    [<Test>]\n    member _.AddSummaryValidationAcceptsGuidIdentifier() =\n        let parameters = AddSummaryParameters(WorkItemId = Guid.NewGuid().ToString(), SummaryContent = \"Summary content\")\n\n        let result = WorkItem.validateAddSummaryParameters parameters\n\n        match result with\n        | Ok _ -> Assert.Pass()\n        | Error errorMessage -> Assert.Fail($\"Expected validation to succeed, but received '{errorMessage}'.\")\n\n    [<Test>]\n    member _.AddSummaryValidationAcceptsNumericIdentifier() =\n        let parameters = AddSummaryParameters(WorkItemId = \"42\", SummaryContent = \"Summary content\")\n\n        let result = WorkItem.validateAddSummaryParameters parameters\n\n        match result with\n        | Ok _ -> Assert.Pass()\n        | Error errorMessage -> Assert.Fail($\"Expected validation to succeed, but received '{errorMessage}'.\")\n\n    [<Test>]\n    member _.AddSummaryValidationRejectsNonPositiveNumericIdentifier() =\n        let parameters = AddSummaryParameters(WorkItemId = \"0\", SummaryContent = \"Summary content\")\n\n        match WorkItem.validateAddSummaryParameters parameters with\n        | Ok _ -> Assert.Fail(\"Expected validation to reject non-positive WorkItemNumber.\")\n        | Error errorMessage -> Assert.That(errorMessage, Does.Contain(WorkItemError.getErrorMessage WorkItemError.InvalidWorkItemNumber))\n\n    [<Test>]\n    member _.AddSummaryValidationRejectsUnsupportedArtifactReferenceMode() =\n        let parameters = AddSummaryParameters(WorkItemId = \"42\", SummaryContent = \"Summary content\", SummaryArtifactId = Guid.NewGuid().ToString())\n\n        match WorkItem.validateAddSummaryParameters parameters with\n        | Ok _ -> Assert.Fail(\"Expected validation to reject caller-supplied artifact IDs.\")\n        | Error errorMessage ->\n            Assert.That(errorMessage, Does.Contain(\"Caller-supplied artifact IDs are not supported\"))\n            Assert.That(errorMessage, Does.Contain(WorkItem.canonicalAddSummaryContractMessage))\n\n    [<Test>]\n    member _.AddSummaryValidationRejectsPromptOriginWithoutPromptContent() =\n        let parameters = AddSummaryParameters(WorkItemId = \"42\", SummaryContent = \"Summary content\", PromptOrigin = \"agent://codex\")\n\n        match WorkItem.validateAddSummaryParameters parameters with\n        | Ok _ -> Assert.Fail(\"Expected validation to reject PromptOrigin when PromptContent is absent.\")\n        | Error errorMessage ->\n            Assert.That(errorMessage, Does.Contain(\"PromptOrigin can only be provided when PromptContent is provided\"))\n            Assert.That(errorMessage, Does.Contain(WorkItem.canonicalAddSummaryContractMessage))\n\n    [<Test>]\n    member _.AddSummaryValidationRejectsInvalidPromotionSetId() =\n        let parameters = AddSummaryParameters(WorkItemId = \"42\", SummaryContent = \"Summary content\", PromotionSetId = \"not-a-guid\")\n\n        match WorkItem.validateAddSummaryParameters parameters with\n        | Ok _ -> Assert.Fail(\"Expected validation to reject invalid PromotionSetId.\")\n        | Error errorMessage -> Assert.That(errorMessage, Is.EqualTo(\"PromotionSetId must be a valid non-empty Guid.\"))\n\n    [<Test>]\n    member _.AddSummaryArtifactSeedNormalizesCorrelationId() =\n        let repositoryId = Guid.Parse(\"89f08f88-0d98-4562-a5f7-bce8d4e4c2ec\")\n        let workItemId = Guid.Parse(\"6d742a8e-5fd6-4d89-81cd-7ea3005570ef\")\n        let lowerSeed = WorkItem.buildAddSummaryArtifactSeed repositoryId workItemId \"corr:add-summary:summary-artifact\"\n        let mixedSeed = WorkItem.buildAddSummaryArtifactSeed repositoryId workItemId \" CoRR:Add-SuMMary:Summary-Artifact \"\n\n        Assert.That(lowerSeed, Is.EqualTo(mixedSeed))\n\n    [<Test>]\n    member _.DeterministicAddSummaryArtifactIdIsStableForReplay() =\n        let repositoryId = Guid.Parse(\"89f08f88-0d98-4562-a5f7-bce8d4e4c2ec\")\n        let workItemId = Guid.Parse(\"6d742a8e-5fd6-4d89-81cd-7ea3005570ef\")\n        let correlationId = \"corr:add-summary:summary-artifact\"\n\n        let firstId = WorkItem.buildDeterministicAddSummaryArtifactId repositoryId workItemId correlationId\n        let replayId = WorkItem.buildDeterministicAddSummaryArtifactId repositoryId workItemId correlationId\n\n        Assert.That(firstId, Is.EqualTo(replayId))\n\n    [<Test>]\n    member _.DeterministicAddSummaryArtifactIdDiffersByArtifactSegment() =\n        let repositoryId = Guid.Parse(\"89f08f88-0d98-4562-a5f7-bce8d4e4c2ec\")\n        let workItemId = Guid.Parse(\"6d742a8e-5fd6-4d89-81cd-7ea3005570ef\")\n\n        let summaryArtifactId = WorkItem.buildDeterministicAddSummaryArtifactId repositoryId workItemId \"corr:add-summary:summary-artifact\"\n\n        let promptArtifactId = WorkItem.buildDeterministicAddSummaryArtifactId repositoryId workItemId \"corr:add-summary:prompt-artifact\"\n\n        Assert.That(summaryArtifactId, Is.Not.EqualTo(promptArtifactId))\n\n    [<Test>]\n    member _.DeterministicAddSummaryBlobPathUsesArtifactIdentityPartition() =\n        let artifactId = Guid.Parse(\"7d535f96-e634-4313-b5ff-d9293ee9db57\")\n        let blobPath = WorkItem.buildDeterministicAddSummaryBlobPath artifactId\n\n        Assert.That(blobPath, Is.EqualTo(\"grace-artifacts/by-id/7d535f96-e634-4313-b5ff-d9293ee9db57\"))\n\n    [<Test>]\n    member _.RemoveArtifactTypeValidationRejectsMissingArtifactType() =\n        let parameters = RemoveArtifactTypeLinksParameters(WorkItemId = Guid.NewGuid().ToString(), ArtifactType = String.Empty)\n\n        let validations = WorkItem.validateRemoveArtifactTypeLinksParameters parameters\n\n        let error =\n            validations\n            |> getFirstError\n            |> Async.AwaitTask\n            |> Async.RunSynchronously\n\n        Assert.That(error, Is.EqualTo(Some WorkItemError.InvalidArtifactType))\n\n    [<Test>]\n    member _.ParseRemovableArtifactTypeHandlesAliases() =\n        let expectedMappings =\n            [\n                \"summary\", ArtifactType.AgentSummary\n                \"agentsummary\", ArtifactType.AgentSummary\n                \"prompt\", ArtifactType.Prompt\n                \"notes\", ArtifactType.ReviewNotes\n                \"reviewnotes\", ArtifactType.ReviewNotes\n            ]\n\n        for (alias, expectedType) in expectedMappings do\n            match WorkItem.parseRemovableArtifactType alias with\n            | Ok artifactType -> Assert.That(artifactType, Is.EqualTo(expectedType))\n            | Error error -> Assert.Fail($\"Expected alias '{alias}' to parse, but received {error}.\")\n\n    [<FsCheck.NUnit.Property(MaxTest = 100)>]\n    member _.WorkItemIdentifierValidationAcceptsPositiveNumberStrings(positiveInt: PositiveInt) =\n        let result =\n            int64 positiveInt.Get\n            |> string\n            |> WorkItem.validateWorkItemIdentifier\n            |> runValidation\n\n        result = (Ok(): Result<unit, WorkItemError>)\n\n    [<FsCheck.NUnit.Property(MaxTest = 100)>]\n    member _.WorkItemIdentifierValidationRejectsNonPositiveNumberStrings(value: int) =\n        let nonPositiveValue = if value > 0 then -value else value\n\n        let result =\n            nonPositiveValue\n            |> string\n            |> WorkItem.validateWorkItemIdentifier\n            |> runValidation\n\n        result = (Error WorkItemError.InvalidWorkItemNumber: Result<unit, WorkItemError>)\n\n    [<FsCheck.NUnit.Property(MaxTest = 100)>]\n    member _.WorkItemIdentifierValidationAcceptsNonEmptyGuidStrings(guid: Guid) =\n        let validGuid = if guid = Guid.Empty then Guid.NewGuid() else guid\n\n        let result =\n            validGuid.ToString()\n            |> WorkItem.validateWorkItemIdentifier\n            |> runValidation\n\n        result = (Ok(): Result<unit, WorkItemError>)\n\n    [<FsCheck.NUnit.Property(MaxTest = 100)>]\n    member _.ParseRemovableArtifactTypeIsCaseInsensitive(pattern: bool array) =\n        let expectedMappings =\n            [\n                \"summary\", ArtifactType.AgentSummary\n                \"agentsummary\", ArtifactType.AgentSummary\n                \"prompt\", ArtifactType.Prompt\n                \"notes\", ArtifactType.ReviewNotes\n                \"reviewnotes\", ArtifactType.ReviewNotes\n            ]\n\n        expectedMappings\n        |> List.forall (fun (alias, expectedType) ->\n            let caseVariant = applyCasePattern pattern alias\n\n            match WorkItem.parseRemovableArtifactType caseVariant with\n            | Ok artifactType -> artifactType = expectedType\n            | Error _ -> false)\n\n    [<Test>]\n    member _.DuplicateCorrelationDetectionFindsMatches() =\n        let timestamp = Instant.FromUtc(2025, 1, 1, 0, 0)\n        let eventMetadata = metadata timestamp\n        let workItemEvent = { Event = WorkItemEventType.TitleSet \"Title\"; Metadata = eventMetadata }\n\n        let duplicate = Grace.Actors.WorkItem.hasDuplicateCorrelationId [ workItemEvent ] eventMetadata\n        let different = Grace.Actors.WorkItem.hasDuplicateCorrelationId [ workItemEvent ] { eventMetadata with CorrelationId = \"corr-other\" }\n\n        Assert.That(duplicate, Is.True)\n        Assert.That(different, Is.False)\n"
  },
  {
    "path": "src/Grace.Server.Tests/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\",\n      \"Microsoft.Hosting.Lifetime\": \"Information\"\n    }\n  },\n  \"AllowedHosts\": \"*\"\n}\n"
  },
  {
    "path": "src/Grace.Shared/AGENTS.md",
    "content": "# Grace.Shared Agents Guide\n\nReview `../AGENTS.md` for global policies before working in this project.\n\n## Purpose\n- Provide reusable utilities, extensions, and cross-cutting helpers consumed by other Grace services, SDKs, and tooling.\n- Centralize shared logic so that features stay DRY and consistent across the solution.\n\n## Key Patterns\n- Organize helpers into modules; keep functions small, pure, and composable wherever possible.\n- Use extension members sparingly and document side-effecting helpers with concise comments so intent stays clear.\n- Maintain `InternalsVisibleTo` patterns that allow targeted testing without leaking unnecessary APIs.\n- Coordinate semantic changes with dependents (`Grace.Server`, `Grace.SDK`, `Grace.CLI`, etc.) to avoid breaking downstream logic.\n- Authentication helpers and parameters live in `Grace.Shared.Parameters.Auth` and `Grace.Types.PersonalAccessToken`; keep token parsing/formatting pure and reusable.\n\n## Project Rules\n1. Prefer adding new helpers instead of altering the semantics of widely used ones—create adapters when behavior must diverge.\n2. When changing internals referenced by other projects, update those callers or provide a compatibility shim.\n3. Record noteworthy patterns or usage notes right here so agents can operate with minimal additional code exploration.\n\n## Validation\n- Extend `Grace.Shared.Tests` (or add new fixtures) whenever helper behavior changes.\n- Run `fantomas` on touched F# files to stay aligned with repo formatting.\n- Execute `dotnet build --configuration Release` to confirm dependent projects continue to compile.\n"
  },
  {
    "path": "src/Grace.Shared/Authorization.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Authorization\nopen Grace.Types.Types\nopen System\n\nmodule Authorization =\n\n    module RoleCatalog =\n\n        let private scopeSystem = \"system\"\n        let private scopeOwner = \"owner\"\n        let private scopeOrganization = \"organization\"\n        let private scopeRepository = \"repository\"\n        let private scopeBranch = \"branch\"\n\n        let private systemAdminOperations =\n            set [ SystemAdmin\n                  OwnerAdmin\n                  OwnerRead\n                  OrgAdmin\n                  OrgRead\n                  RepoAdmin\n                  RepoRead\n                  RepoWrite\n                  BranchAdmin\n                  BranchRead\n                  BranchWrite\n                  PathRead\n                  PathWrite ]\n\n        let private ownerAdminOperations =\n            set [ OwnerAdmin\n                  OwnerRead\n                  OrgAdmin\n                  OrgRead\n                  RepoAdmin\n                  RepoRead\n                  RepoWrite\n                  BranchAdmin\n                  BranchRead\n                  BranchWrite\n                  PathRead\n                  PathWrite ]\n\n        let private ownerReaderOperations =\n            set [ OwnerRead\n                  OrgRead\n                  RepoRead\n                  BranchRead\n                  PathRead ]\n\n        let private orgAdminOperations =\n            set [ OrgAdmin\n                  OrgRead\n                  RepoAdmin\n                  RepoWrite\n                  RepoRead\n                  PathWrite\n                  PathRead\n                  BranchAdmin\n                  BranchWrite\n                  BranchRead ]\n\n        let private orgReaderOperations =\n            set [ OrgRead\n                  RepoRead\n                  PathRead\n                  BranchRead ]\n\n        let private repoAdminOperations =\n            set [ RepoAdmin\n                  RepoWrite\n                  RepoRead\n                  PathWrite\n                  PathRead\n                  BranchAdmin\n                  BranchWrite\n                  BranchRead ]\n\n        let private repoContributorOperations =\n            set [ RepoWrite\n                  RepoRead\n                  PathWrite\n                  PathRead\n                  BranchWrite\n                  BranchRead ]\n\n        let private repoReaderOperations = set [ RepoRead; PathRead; BranchRead ]\n\n        let private branchAdminOperations =\n            set [ BranchAdmin\n                  BranchWrite\n                  BranchRead ]\n\n        let private branchWriterOperations =\n            set [ BranchWrite\n                  BranchRead ]\n\n        let private branchReaderOperations =\n            set [ BranchRead ]\n\n        let private roles: RoleDefinition list =\n            [\n                { RoleId = \"SystemAdmin\"; AllowedOperations = systemAdminOperations; AppliesTo = Set.ofList [ scopeSystem ] }\n                { RoleId = \"OwnerAdmin\"; AllowedOperations = ownerAdminOperations; AppliesTo = Set.ofList [ scopeOwner ] }\n                { RoleId = \"OwnerReader\"; AllowedOperations = ownerReaderOperations; AppliesTo = Set.ofList [ scopeOwner ] }\n                { RoleId = \"OrgAdmin\"; AllowedOperations = orgAdminOperations; AppliesTo = Set.ofList [ scopeOrganization ] }\n                { RoleId = \"OrgReader\"; AllowedOperations = orgReaderOperations; AppliesTo = Set.ofList [ scopeOrganization ] }\n                { RoleId = \"RepoAdmin\"; AllowedOperations = repoAdminOperations; AppliesTo = Set.ofList [ scopeRepository ] }\n                { RoleId = \"RepoContributor\"; AllowedOperations = repoContributorOperations; AppliesTo = Set.ofList [ scopeRepository ] }\n                { RoleId = \"RepoReader\"; AllowedOperations = repoReaderOperations; AppliesTo = Set.ofList [ scopeRepository ] }\n                { RoleId = \"BranchAdmin\"; AllowedOperations = branchAdminOperations; AppliesTo = Set.ofList [ scopeBranch ] }\n                { RoleId = \"BranchWriter\"; AllowedOperations = branchWriterOperations; AppliesTo = Set.ofList [ scopeBranch ] }\n                { RoleId = \"BranchReader\"; AllowedOperations = branchReaderOperations; AppliesTo = Set.ofList [ scopeBranch ] }\n            ]\n\n        let getAll () = roles\n\n        let tryGet (roleId: RoleId) =\n            roles\n            |> List.tryFind (fun role -> role.RoleId.Equals(roleId, StringComparison.OrdinalIgnoreCase))\n\n    let scopesForResource (resource: Resource) =\n        match resource with\n        | Resource.System -> [ Scope.System ]\n        | Resource.Owner ownerId -> [ Scope.Owner ownerId; Scope.System ]\n        | Resource.Organization (ownerId, organizationId) ->\n            [\n                Scope.Organization(ownerId, organizationId)\n                Scope.Owner ownerId\n                Scope.System\n            ]\n        | Resource.Repository (ownerId, organizationId, repositoryId) ->\n            [\n                Scope.Repository(ownerId, organizationId, repositoryId)\n                Scope.Organization(ownerId, organizationId)\n                Scope.Owner ownerId\n                Scope.System\n            ]\n        | Resource.Branch (ownerId, organizationId, repositoryId, branchId) ->\n            [\n                Scope.Branch(ownerId, organizationId, repositoryId, branchId)\n                Scope.Repository(ownerId, organizationId, repositoryId)\n                Scope.Organization(ownerId, organizationId)\n                Scope.Owner ownerId\n                Scope.System\n            ]\n        | Resource.Path (ownerId, organizationId, repositoryId, _relativePath) ->\n            [\n                Scope.Repository(ownerId, organizationId, repositoryId)\n                Scope.Organization(ownerId, organizationId)\n                Scope.Owner ownerId\n                Scope.System\n            ]\n\n    let private scopeKind (scope: Scope) =\n        match scope with\n        | Scope.System -> \"system\"\n        | Scope.Owner _ -> \"owner\"\n        | Scope.Organization _ -> \"organization\"\n        | Scope.Repository _ -> \"repository\"\n        | Scope.Branch _ -> \"branch\"\n\n    let effectiveOperations (roleCatalog: RoleDefinition list) (assignments: RoleAssignment list) (principalSet: Principal list) (resource: Resource) =\n        let scopeSet = scopesForResource resource |> Set.ofList\n        let principalLookup = principalSet |> Set.ofList\n\n        assignments\n        |> List.choose (fun assignment ->\n            if principalLookup.Contains assignment.Principal\n               && scopeSet.Contains assignment.Scope then\n                let scopeKind = scopeKind assignment.Scope\n\n                roleCatalog\n                |> List.tryFind (fun role ->\n                    role.RoleId.Equals(assignment.RoleId, StringComparison.OrdinalIgnoreCase)\n                    && role.AppliesTo.Contains scopeKind)\n            else\n                None)\n        |> List.collect (fun role -> role.AllowedOperations |> Set.toList)\n        |> Set.ofList\n\n    let checkPathPermission (pathPermissions: PathPermission list) (effectiveClaims: Set<string>) (targetPath: RelativePath) (operation: Operation) =\n        match operation with\n        | PathRead\n        | PathWrite ->\n            let normalizedTarget = normalizeFilePath targetPath\n\n            let matchingClaimPermissions =\n                pathPermissions\n                |> List.filter (fun pathPermission -> normalizeFilePath pathPermission.Path = normalizedTarget)\n                |> List.collect (fun pathPermission -> pathPermission.Permissions |> Seq.toList)\n                |> List.filter (fun permission -> effectiveClaims.Contains permission.Claim)\n\n            let hasDeny =\n                matchingClaimPermissions\n                |> List.exists (fun permission -> permission.DirectoryPermission = DirectoryPermission.NoAccess)\n\n            if hasDeny then\n                Some(Denied($\"Denied by path permission at '{normalizedTarget}'.\"))\n            else\n                let allowsRead, allowsWrite =\n                    matchingClaimPermissions\n                    |> List.fold\n                        (fun (readAllowed, writeAllowed) permission ->\n                            match permission.DirectoryPermission with\n                            | DirectoryPermission.FullControl\n                            | DirectoryPermission.Modify -> true, true\n                            | DirectoryPermission.Read\n                            | DirectoryPermission.ListContents -> true, writeAllowed\n                            | DirectoryPermission.NoAccess\n                            | DirectoryPermission.NotSet -> readAllowed, writeAllowed)\n                        (false, false)\n\n                match operation with\n                | PathRead when allowsRead -> Some(Allowed($\"Allowed by path permission at '{normalizedTarget}'.\"))\n                | PathWrite when allowsWrite -> Some(Allowed($\"Allowed by path permission at '{normalizedTarget}'.\"))\n                | _ -> None\n        | _ -> None\n\n    let checkPermission\n        (roleCatalog: RoleDefinition list)\n        (assignments: RoleAssignment list)\n        (pathPermissions: PathPermission list)\n        (principalSet: Principal list)\n        (effectiveClaims: Set<string>)\n        (operation: Operation)\n        (resource: Resource)\n        =\n        let pathDecision =\n            match resource with\n            | Resource.Path (_, _, _, relativePath) -> checkPathPermission pathPermissions effectiveClaims relativePath operation\n            | _ -> None\n\n        match pathDecision with\n        | Some decision -> decision\n        | None ->\n            let effectiveOps = effectiveOperations roleCatalog assignments principalSet resource\n\n            if effectiveOps.Contains operation then\n                Allowed $\"Allowed by role assignments for {operation}.\"\n            else\n                Denied $\"Denied: missing permission {operation}.\"\n"
  },
  {
    "path": "src/Grace.Shared/AzureEnvironment.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen System\nopen System.Collections.Generic\n\nmodule AzureEnvironment =\n\n    type StorageEndpoints = { BlobEndpoint: Uri; QueueEndpoint: Uri; TableEndpoint: Uri; AccountName: string; ConnectionString: string option }\n\n    let private tryGetEnv (name: string) =\n        let value = Environment.GetEnvironmentVariable(name)\n        if String.IsNullOrWhiteSpace value then None else Some(value.Trim())\n\n    let private parseConnectionString (value: string option) =\n        let dictionary = Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n\n        match value with\n        | None -> dictionary\n        | Some raw ->\n            for segment in raw.Split([| ';' |], StringSplitOptions.RemoveEmptyEntries) do\n                let idx = segment.IndexOf('=')\n\n                if idx > 0 then\n                    let key = segment.Substring(0, idx).Trim()\n                    let data = segment.Substring(idx + 1).Trim()\n\n                    if key.Length > 0 && data.Length > 0 then dictionary[key] <- data\n\n            dictionary\n\n    let private tryGetValue key (dictionary: Dictionary<string, string>) =\n        match dictionary.TryGetValue(key) with\n        | true, value when not (String.IsNullOrWhiteSpace value) -> Some(value.Trim())\n        | _ -> None\n\n    let private storageConnectionStringValue = tryGetEnv Constants.EnvironmentVariables.AzureStorageConnectionString\n    let private storageSegments = parseConnectionString storageConnectionStringValue\n\n    let private cosmosConnectionStringValue = tryGetEnv Constants.EnvironmentVariables.AzureCosmosDBConnectionString\n    let private cosmosSegments = parseConnectionString cosmosConnectionStringValue\n\n    let private serviceBusConnectionStringValue = tryGetEnv Constants.EnvironmentVariables.AzureServiceBusConnectionString\n    let private serviceBusSegments = parseConnectionString serviceBusConnectionStringValue\n\n    let debugEnvironment = tryGetEnv Constants.EnvironmentVariables.DebugEnvironment\n\n    let useManagedIdentity =\n        match debugEnvironment with\n        | Some value when value.Equals(\"Local\", StringComparison.OrdinalIgnoreCase) -> false\n        | _ -> true\n\n    let useManagedIdentityForStorage =\n        useManagedIdentity\n        && storageConnectionStringValue.IsNone\n\n    let useManagedIdentityForCosmos =\n        useManagedIdentity\n        && cosmosConnectionStringValue.IsNone\n\n    let useManagedIdentityForServiceBus =\n        useManagedIdentity\n        && serviceBusConnectionStringValue.IsNone\n\n    let private getStorageAccountName () =\n        tryGetEnv Constants.EnvironmentVariables.AzureStorageAccountName\n        |> Option.orElse (tryGetValue \"AccountName\" storageSegments)\n        |> function\n            | Some name -> name\n            | None when useManagedIdentity ->\n                invalidOp \"Azure Storage account name must be provided via grace__azure_storage__account_name when using a managed identity.\"\n            | None -> Constants.DefaultObjectStorageAccount\n\n    let private getStorageEndpointSuffix () =\n        tryGetEnv Constants.EnvironmentVariables.AzureStorageEndpointSuffix\n        |> Option.orElse (tryGetValue \"EndpointSuffix\" storageSegments)\n        |> Option.defaultValue \"core.windows.net\"\n\n    let private ensureUri (value: string) =\n        let trimmed = value.Trim().TrimEnd('/')\n        Uri(trimmed, UriKind.Absolute)\n\n    let private buildStorageUri service endpointKey =\n        match tryGetValue endpointKey storageSegments with\n        | Some endpoint -> ensureUri (endpoint)\n        | None ->\n            let accountName = getStorageAccountName ()\n            let suffix = getStorageEndpointSuffix ()\n            ensureUri $\"https://{accountName}.{service}.{suffix}\"\n\n    let storageEndpoints =\n        {\n            BlobEndpoint = buildStorageUri \"blob\" \"BlobEndpoint\"\n            QueueEndpoint = buildStorageUri \"queue\" \"QueueEndpoint\"\n            TableEndpoint = buildStorageUri \"table\" \"TableEndpoint\"\n            AccountName = getStorageAccountName ()\n            ConnectionString = storageConnectionStringValue\n        }\n\n    let storageAccountKey =\n        tryGetEnv Constants.EnvironmentVariables.AzureStorageKey\n        |> Option.orElse (tryGetValue \"AccountKey\" storageSegments)\n\n    let cosmosConnectionString = cosmosConnectionStringValue\n\n    let tryGetCosmosEndpointUri () =\n        tryGetEnv Constants.EnvironmentVariables.AzureCosmosDBEndpoint\n        |> Option.map ensureUri\n        |> Option.orElse (\n            tryGetValue \"AccountEndpoint\" cosmosSegments\n            |> Option.map ensureUri\n        )\n\n    let tryGetServiceBusConnectionString () = serviceBusConnectionStringValue\n\n    let private normalizeServiceBusNamespace (value: string) =\n        let trimmed = value.Trim()\n\n        let withoutScheme =\n            if trimmed.StartsWith(\"sb://\", StringComparison.OrdinalIgnoreCase) then\n                trimmed.Substring(5)\n            else\n                trimmed\n\n        let normalizedNamespace = withoutScheme.Trim().TrimEnd('/')\n\n        if normalizedNamespace.Contains(\".\") then\n            normalizedNamespace\n        else\n            $\"{normalizedNamespace}.servicebus.windows.net\"\n\n    let tryGetServiceBusFullyQualifiedNamespace () =\n        match tryGetEnv Constants.EnvironmentVariables.AzureServiceBusNamespace with\n        | Some value -> Some(normalizeServiceBusNamespace value)\n        | None ->\n            match tryGetValue \"Endpoint\" serviceBusSegments with\n            | Some endpoint -> Some(normalizeServiceBusNamespace endpoint)\n            | None -> None\n"
  },
  {
    "path": "src/Grace.Shared/BaselineDrift.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen Grace.Types.Policy\nopen Grace.Types.Review\nopen Grace.Types.Types\n\nmodule BaselineDrift =\n    type BaselineDriftResult = { IsMeaningful: bool; AffectedPaths: RelativePath list; AffectedChapterIds: ChapterId list; AffectedFindingIds: FindingId list }\n\n    let evaluate (policy: PolicySnapshot) (riskProfile: DeterministicRiskProfile) (chapters: Chapter list) (findings: Finding list) =\n        let churnLines =\n            riskProfile.Churn.LinesAdded\n            + riskProfile.Churn.LinesRemoved\n\n        let filesTouched = riskProfile.Churn.FilesChanged\n        let threshold = policy.Rules.ApprovalRules.BaselineDriftReackThreshold\n\n        let isMeaningful =\n            churnLines >= threshold.ChurnLines\n            || filesTouched >= threshold.FilesTouched\n\n        let affectedPaths =\n            riskProfile.ChangedPaths\n            |> List.map (fun path -> path.RelativePath)\n            |> List.distinct\n\n        let affectedChapters =\n            chapters\n            |> List.filter (fun chapter ->\n                chapter.Paths\n                |> List.exists (fun path -> affectedPaths |> List.contains path))\n            |> List.map (fun chapter -> chapter.ChapterId)\n\n        let affectedFindings =\n            findings\n            |> List.filter (fun finding ->\n                finding.EvidenceReferences\n                |> List.exists (fun evidence ->\n                    affectedPaths\n                    |> List.contains evidence.RelativePath))\n            |> List.map (fun finding -> finding.FindingId)\n\n        { IsMeaningful = isMeaningful; AffectedPaths = affectedPaths; AffectedChapterIds = affectedChapters; AffectedFindingIds = affectedFindings }\n"
  },
  {
    "path": "src/Grace.Shared/Client/Configuration.Shared.fs",
    "content": "namespace Grace.Shared.Client\n\nopen Grace.Shared.Client.Theme\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen System\nopen System.Collections.Generic\nopen System.Diagnostics\nopen System.IO\nopen System.Text.Json\nopen System.Text.Json.Serialization\nopen System.Runtime.InteropServices\n\nmodule Configuration =\n\n    let writeNewConfiguration = false\n\n    /// The global, client-side configuration object for Grace.\n    // GraceConfiguration is implemented as a class, rather than a record, to allow for less brittle JSON serialization and deserialization of the configuration file.\n    // Records don't handle missing values well during deserialization.\n    type GraceConfiguration() =\n        /// The OwnerId of the current repository.\n        member val public OwnerId: OwnerId = OwnerId.Empty with get, set\n        /// The OwnerName of the current repository.\n        member val public OwnerName = OwnerName String.Empty with get, set\n        /// The OrganizationId of the current repository.\n        member val public OrganizationId: OrganizationId = OrganizationId.Empty with get, set\n        /// The OrganizationName of the current repository.\n        member val public OrganizationName = OrganizationName String.Empty with get, set\n        /// The RepositoryId of the current repository.\n        member val public RepositoryId: RepositoryId = RepositoryId.Empty with get, set\n        /// The name of the current repository.\n        member val public RepositoryName = RepositoryName String.Empty with get, set\n        /// The BranchId of the current branch.\n        member val public BranchId: BranchId = BranchId.Empty with get, set\n        /// The BranchName of the current branch.\n        member val public BranchName = BranchName String.Empty with get, set\n        /// The name of the default branch in the current repository.\n        member val public DefaultBranchName: BranchName = String.Empty with get, set\n        /// The color themes available in Grace.\n        member val public Themes = [| Theme.DefaultTheme |] with get, set\n        /// The style of line endings that Grace should expect to handle for the current repository.\n        member val public LineEndings = LineEndings.PlatformDependent.ToString() with get, set\n        /// A list of branch names to prefetch whenever they have new commits.\n        member val public Prefetch = [| String.Empty |] with get, set\n        /// The local root directory of the current repository.\n        member val public RootDirectory = Environment.CurrentDirectory with get, set\n        /// The local root directory of the current repository, rendered with '/' as a path separator.\n        member val public StandardizedRootDirectory = normalizeFilePath Environment.CurrentDirectory with get, set\n        /// The Grace (/.grace) directory path in this repository.\n        member val public GraceDirectory = Environment.CurrentDirectory with get, set\n        /// The Grace objects (/.grace/objects) directory path. This is where Grace keeps locally-cached versions of repository artifacts.\n        member val public ObjectDirectory = Environment.CurrentDirectory with get, set\n        /// The location of the Grace index file.\n        member val public GraceStatusFile = Constants.GraceLocalStateDbFileName with get, set\n        /// The location of the Grace object cache file.\n        member val public GraceObjectCacheFile = Constants.GraceLocalStateDbFileName with get, set\n        /// The Grace objects directory cache path. This is where Grace keeps locally-cached DirectoryVersion's.\n        member val public DirectoryVersionCache = Environment.CurrentDirectory with get, set\n        /// The local directory where graceconfig.json is found for this repository.\n        member val public ConfigurationDirectory = Environment.CurrentDirectory with get, set\n        /// The blob storage provider used by this instance of Grace to store files.\n        member val public ObjectStorageProvider = ObjectStorageProvider.Unknown with get, set\n        /// The Uri of the instance of Grace Server used by the current repository.\n        member val public ServerUri = @\"http://127.0.0.1:5000\" with get, set\n        /// This version of Grace.\n        member val public ProgramVersion = Constants.CurrentConfigurationVersion with get, set\n        /// The current format of configuration.\n        member val public ConfigurationVersion = String.Empty with get, set\n\n        /// The current list of graceignore.json entries.\n        [<JsonIgnore(Condition = JsonIgnoreCondition.Always)>]\n        member val public GraceIgnoreEntries = [| String.Empty |] with get, set\n\n        /// The current list of graceignore.json entries for files.\n        [<JsonIgnore(Condition = JsonIgnoreCondition.Always)>]\n        member val public GraceFileIgnoreEntries = [| String.Empty |] with get, set\n\n        /// The current list of graceignore.json entries for directories.\n        [<JsonIgnore(Condition = JsonIgnoreCondition.Always)>]\n        member val public GraceDirectoryIgnoreEntries = [| String.Empty |] with get, set\n        // /// The list of aliases for the Grace CLI.\n        // member val public Aliases = Dictionary<string, string[]>() with get, set\n        /// Indicates that this instance of GraceConfiguration has been populated.\n        [<JsonIgnore(Condition = JsonIgnoreCondition.Always)>]\n        member val public IsPopulated = false with get, set\n\n        override this.ToString() = serialize this\n\n    let mutable private graceConfiguration = GraceConfiguration()\n\n    let saveConfigFile graceConfigurationFilePath (graceConfiguration: GraceConfiguration) =\n        try\n            let json = serialize graceConfiguration\n            File.WriteAllText(graceConfigurationFilePath, json)\n        with\n        | ex -> printfn $\"Exception: {ex.Message}{Environment.NewLine}Stack trace: {ex.StackTrace}\"\n\n    let private findGraceConfigurationFile () =\n        try\n            let mutable currentDirectory = DirectoryInfo(Environment.CurrentDirectory)\n\n            if currentDirectory.FullName = Environment.SystemDirectory then\n                // This happens when this is running from a Windows app, and we want to know where it is\n                currentDirectory <- DirectoryInfo(Path.GetDirectoryName(Environment.ProcessPath))\n            // Other ways I've tried to get the current directory:\n            // let mutable currentDirectory = DirectoryInfo(Path.GetDirectoryName(Environment.ProcessPath))\n            // let mutable currentDirectory = DirectoryInfo(Process.GetCurrentProcess().StartInfo.WorkingDirectory)\n            let mutable graceConfigPath = String.Empty\n\n            while String.IsNullOrEmpty(graceConfigPath)\n                  && not (isNull currentDirectory) do\n                let fullPath = Path.Combine(currentDirectory.FullName, Constants.GraceConfigDirectory, Constants.GraceConfigFileName)\n                //printfn $\"Searching for configuration in {currentDirectory}...\"\n                if File.Exists(fullPath) then\n                    graceConfigPath <- fullPath\n                //printfn $\"Found Grace configuration file at {fullPath}.{Environment.NewLine}{Constants.OutputDelimiter}\"\n                else\n                    currentDirectory <- currentDirectory.Parent\n\n            if not (String.IsNullOrEmpty(graceConfigPath)) then\n                Ok graceConfigPath\n            else\n                //let graceConfigPath = Path.Combine(Environment.CurrentDirectory, Constants.GraceConfigDirectory, Constants.GraceConfigFileName)\n                //Directory.CreateDirectory(FileInfo(graceConfigPath).DirectoryName) |> ignore\n                //saveDefaultConfig graceConfigPath\n                //Result.Ok graceConfigPath\n                Error $\"No {Constants.GraceConfigFileName} file found along current path. Please run `grace config write` to create one.\"\n\n        with\n        | :? System.IO.IOException as ex -> Error $\"Exception while parsing directory paths: {ex.Message}\"\n        | ex -> Error $\"Exception: {ex.Message}\"\n\n    let configurationFileExists () =\n        match findGraceConfigurationFile () with\n        | Ok _ -> true\n        | Error _ -> false\n\n    let private parseConfigurationFile graceConfigurationFilePath =\n        try\n            // Read configuration into a stream from file path specified by graceConfigurationFilePath\n            use stream = new FileStream(graceConfigurationFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)\n            // Read the stream into a buffer\n            let buffer = Array.zeroCreate<byte> (int stream.Length)\n            stream.Read(buffer, 0, buffer.Length) |> ignore\n            // Deserialize the JSON configuration file into a GraceConfiguration object\n            let graceConfiguration = JsonSerializer.Deserialize<GraceConfiguration>(buffer, Constants.JsonSerializerOptions)\n\n            Ok graceConfiguration\n        with\n        | ex -> Error $\"Exception: {ex.Message}{Environment.NewLine}Stack trace: {ex.StackTrace}\"\n\n    let private getGraceIgnoreEntries graceIgnorePath =\n        if File.Exists(graceIgnorePath) then\n            File.ReadAllLines(graceIgnorePath)\n            |> Seq.map (fun graceIgnoreLine ->\n                let commentIndex = graceIgnoreLine.IndexOf('#')\n\n                if commentIndex = -1 then\n                    graceIgnoreLine.Trim()\n                else\n                    graceIgnoreLine.Substring(0, commentIndex).Trim())\n            |> Seq.filter (fun graceIgnoreLine -> (not (String.IsNullOrEmpty(graceIgnoreLine))))\n            |> Seq.map (fun graceIgnoreLine -> Path.TrimEndingDirectorySeparator(graceIgnoreLine))\n            |> Seq.toArray\n        else\n            Array.empty\n\n    let private populateDerivedFields (graceConfigurationFilePath: string) (graceConfiguration: GraceConfiguration) =\n        let graceConfigurationDirectory = Path.GetDirectoryName(graceConfigurationFilePath)\n\n        graceConfiguration.RootDirectory <- Path.GetFullPath(Path.Combine(graceConfigurationDirectory, \"..\"))\n        graceConfiguration.GraceDirectory <- Path.GetFullPath(graceConfigurationDirectory)\n\n        graceConfiguration.ObjectDirectory <- Path.GetFullPath(Path.Combine(graceConfigurationDirectory, Constants.GraceObjectsDirectory))\n\n        let graceLocalStateDbPath =\n            Path.Combine(graceConfiguration.GraceDirectory, Constants.GraceLocalStateDbFileName)\n\n        graceConfiguration.GraceStatusFile <- graceLocalStateDbPath\n        graceConfiguration.GraceObjectCacheFile <- graceLocalStateDbPath\n\n        graceConfiguration.DirectoryVersionCache <- Path.GetFullPath(Path.Combine(graceConfigurationDirectory, Constants.GraceDirectoryVersionCacheName))\n\n        graceConfiguration.ConfigurationDirectory <- FileInfo(graceConfigurationFilePath).DirectoryName\n\n        let graceIgnoreFullPath = (Path.Combine(graceConfiguration.RootDirectory, Constants.GraceIgnoreFileName))\n\n        let graceIgnoreEntries = getGraceIgnoreEntries graceIgnoreFullPath\n\n        graceConfiguration.GraceIgnoreEntries <- graceIgnoreEntries\n\n        graceConfiguration.GraceFileIgnoreEntries <-\n            graceIgnoreEntries\n            |> Array.where (fun graceIgnoreLine -> not <| pathContainsSeparator graceIgnoreLine)\n\n        graceConfiguration.GraceDirectoryIgnoreEntries <-\n            graceIgnoreEntries\n            |> Array.where (fun graceIgnoreLine -> pathContainsSeparator graceIgnoreLine)\n            |> Array.map (fun graceIgnoreLine -> $\"*{graceIgnoreLine}\")\n\n        graceConfiguration.IsPopulated <- true\n        graceConfiguration\n\n    let private createDefaultConfigurationFile () =\n        let graceConfigurationFilePath = Path.Combine(Environment.CurrentDirectory, Constants.GraceConfigDirectory, Constants.GraceConfigFileName)\n\n        Directory.CreateDirectory(Path.GetDirectoryName(graceConfigurationFilePath))\n        |> ignore\n\n        let newConfiguration = GraceConfiguration()\n        saveConfigFile graceConfigurationFilePath newConfiguration\n        graceConfigurationFilePath, newConfiguration\n\n    let private isCurrentDirectoryWithinRoot (rootDirectory: string) =\n        if String.IsNullOrWhiteSpace rootDirectory then\n            false\n        else\n            let normalize (path: string) =\n                Path\n                    .GetFullPath(path)\n                    .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)\n                + string Path.DirectorySeparatorChar\n\n            let normalizedRoot = normalize rootDirectory\n            let normalizedCurrent = normalize Environment.CurrentDirectory\n\n            normalizedCurrent.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)\n\n    let private getGraceConfiguration () =\n        if graceConfiguration.IsPopulated\n           && isCurrentDirectoryWithinRoot graceConfiguration.RootDirectory then\n            graceConfiguration\n        else\n            match findGraceConfigurationFile () with\n            | Ok graceConfigurationFilePath ->\n#if DEBUG\n                if writeNewConfiguration then\n                    GraceConfiguration()\n                    |> saveConfigFile graceConfigurationFilePath\n#endif\n                match (parseConfigurationFile graceConfigurationFilePath) with\n                | Ok graceConfigurationFromFile ->\n                    graceConfiguration <- graceConfigurationFromFile\n                    graceConfiguration <- populateDerivedFields graceConfigurationFilePath graceConfiguration\n                    graceConfiguration\n                | Result.Error errorMessage ->\n                    printfn $\"{errorMessage}\"\n                    exit Results.InvalidConfigurationFile\n            | Result.Error errorMessage ->\n                // We didn't find a graceconfig.json file, so we'll create a default one on disk just to finish the command.\n                printfn $\"{errorMessage}\"\n                let graceConfigurationFilePath, newConfiguration = createDefaultConfigurationFile ()\n                graceConfiguration <- populateDerivedFields graceConfigurationFilePath newConfiguration\n                graceConfiguration\n\n    // graceConfiguration <- GraceConfiguration()\n    // graceConfiguration.IsPopulated <- true\n    // graceConfiguration\n\n    /// The current configuration of Grace in this repository.\n    let Current () = getGraceConfiguration ()\n\n    let resetConfiguration () = graceConfiguration.IsPopulated <- false\n\n    /// Saves the Grace configuration file after updates. Makes a backup of the previous version of the file.\n    let updateConfiguration (newConfiguration: GraceConfiguration) =\n        do\n            File.Copy(\n                Path.Combine(Current().ConfigurationDirectory, Constants.GraceConfigFileName),\n                Path.Combine(Current().ConfigurationDirectory, $\"{Constants.GraceConfigFileName}.backup\"),\n                overwrite = true\n            )\n\n        newConfiguration\n        |> saveConfigFile (Path.Combine(Current().ConfigurationDirectory, Constants.GraceConfigFileName))\n\n        graceConfiguration <- newConfiguration\n\n    module Colors =\n        let themes =\n            if configurationFileExists () then\n                Current().Themes\n            else\n                [| Theme.DefaultTheme |]\n\n        let theme = themes[0]\n        let Added = theme.DisplayColorOptions[DisplayColor.Added]\n        let Deemphasized = theme.DisplayColorOptions[DisplayColor.Deemphasized]\n        let Deleted = theme.DisplayColorOptions[DisplayColor.Deleted]\n        let Changed = theme.DisplayColorOptions[DisplayColor.Changed]\n        let Error = theme.DisplayColorOptions[DisplayColor.Error]\n        let Important = theme.DisplayColorOptions[DisplayColor.Important]\n        let Highlighted = theme.DisplayColorOptions[DisplayColor.Highlighted]\n        let Verbose = theme.DisplayColorOptions[DisplayColor.Verbose]\n"
  },
  {
    "path": "src/Grace.Shared/Client/Theme.Shared.fs",
    "content": "namespace Grace.Shared.Client\n\nopen System\nopen System.Drawing\nopen System.Collections.Generic\n\n// A few comments on Theme:\n// It's \"optimized\" for being able to add #RRGGBB values to the configuration easily.\n\nmodule Theme =\n\n    module DisplayColor =\n        let Added = \"added\"\n        let Changed = \"changed\"\n        let Deemphasized = \"deemphasized\"\n        let Deleted = \"deleted\"\n        let Error = \"error\"\n        let Highlighted = \"highlighted\"\n        let Important = \"important\"\n        let Verbose = \"verbose\"\n\n    let format (color: Color) = $\"#{color.R:X2}{color.G:X2}{color.B:X2}\"\n\n    type Theme =\n        {\n            Name: string\n            DisplayColorOptions: IReadOnlyDictionary<string, string>\n        }\n\n        override this.ToString() = $\"{this.Name}\"\n\n        static member Create (name: string) (colors: Color []) =\n            let displayColorOptions = Dictionary<string, string>()\n            displayColorOptions.Add(DisplayColor.Added, format colors[0])\n            displayColorOptions.Add(DisplayColor.Changed, format colors[1])\n            displayColorOptions.Add(DisplayColor.Deemphasized, format colors[2])\n            displayColorOptions.Add(DisplayColor.Deleted, format colors[3])\n            displayColorOptions.Add(DisplayColor.Error, format colors[4])\n            displayColorOptions.Add(DisplayColor.Highlighted, format colors[5])\n            displayColorOptions.Add(DisplayColor.Important, format colors[6])\n            displayColorOptions.Add(DisplayColor.Verbose, format colors[7])\n\n            { Name = name; DisplayColorOptions = displayColorOptions }\n\n    let private defaultColors =\n        [|\n            Color.FromArgb(0x00, 0xaf, 0x5f)\n            Color.Purple\n            Color.Gray\n            Color.DarkRed\n            Color.Red\n            Color.White\n            Color.FromArgb(0xe5, 0xc0, 0x7b)\n            Color.FromArgb(0xff, 0x7e, 0x00)\n        |]\n\n    /// Default color theme, with green for adds, red for deletes, etc.\n    let DefaultTheme = Theme.Create \"Default\" defaultColors\n"
  },
  {
    "path": "src/Grace.Shared/Client/UserConfiguration.Shared.fs",
    "content": "namespace Grace.Shared.Client\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen System\nopen System.IO\nopen System.Text.Json\n\nmodule UserConfiguration =\n\n    [<Literal>]\n    let private DefaultMaxEntries = 5000\n\n    [<Literal>]\n    let private DefaultMaxFileBytes = 10L * 1024L * 1024L\n\n    [<Literal>]\n    let private DefaultRetentionDays = 90\n\n    let defaultRedactOptionNames () =\n        [|\n            \"token\"\n            \"sas\"\n            \"sig\"\n            \"password\"\n            \"connection-string\"\n            \"connectionstring\"\n        |]\n\n    let defaultRedactRegexes () =\n        [|\n            \"(?i)(sig=)[^&]+\"\n            \"(?i)(token=)[^&]+\"\n            \"(?i)(password=)[^&]+\"\n        |]\n\n    let defaultDestructiveTokenRegexes () =\n        [|\n            \"(?i)\\\\b(delete|remove|purge|destroy|reset|drop)\\\\b\"\n        |]\n\n    type HistoryConfiguration() =\n        member val Enabled = false with get, set\n        member val MaxEntries = DefaultMaxEntries with get, set\n        member val MaxFileBytes = DefaultMaxFileBytes with get, set\n        member val RetentionDays = DefaultRetentionDays with get, set\n        member val RecordHistoryCommands = false with get, set\n        member val RedactOptionNames = defaultRedactOptionNames () with get, set\n        member val RedactRegexes = defaultRedactRegexes () with get, set\n        member val DestructiveTokenRegexes = defaultDestructiveTokenRegexes () with get, set\n\n    type AuthConfiguration() =\n        member val ActiveAccountId = String.Empty with get, set\n        member val ActiveTenantId = String.Empty with get, set\n        member val ActiveUsername = String.Empty with get, set\n\n    type UserConfiguration() =\n        member val History = HistoryConfiguration() with get, set\n        member val Auth = AuthConfiguration() with get, set\n\n        override this.ToString() = serialize this\n\n    type UserConfigurationLoadResult = { Configuration: UserConfiguration; WasCorrupt: bool; ErrorMessage: string option; CreatedNew: bool }\n\n    let private normalizeHistory (history: HistoryConfiguration) =\n        let normalized = if obj.ReferenceEquals(history, null) then HistoryConfiguration() else history\n\n        if isNull normalized.RedactOptionNames then\n            normalized.RedactOptionNames <- defaultRedactOptionNames ()\n\n        if isNull normalized.RedactRegexes then\n            normalized.RedactRegexes <- defaultRedactRegexes ()\n\n        if isNull normalized.DestructiveTokenRegexes then\n            normalized.DestructiveTokenRegexes <- defaultDestructiveTokenRegexes ()\n\n        normalized\n\n    let private normalizeConfiguration (configuration: UserConfiguration) =\n        let normalized =\n            if obj.ReferenceEquals(configuration, null) then\n                UserConfiguration()\n            else\n                configuration\n\n        normalized.History <- normalizeHistory normalized.History\n\n        normalized.Auth <-\n            if obj.ReferenceEquals(normalized.Auth, null) then\n                AuthConfiguration()\n            else\n                normalized.Auth\n\n        normalized\n\n    let private getUserProfileDirectory () =\n        let userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)\n\n        if not <| String.IsNullOrWhiteSpace(userProfile) then\n            userProfile\n        else\n            let home = Environment.GetEnvironmentVariable(\"HOME\")\n\n            if not <| String.IsNullOrWhiteSpace(home) then\n                home\n            else\n                Environment.CurrentDirectory\n\n    let getUserGraceDirectory () =\n        let userProfile = getUserProfileDirectory ()\n        Path.Combine(userProfile, \".grace\")\n\n    let getUserConfigurationPath () =\n        let userGraceDir = getUserGraceDirectory ()\n        Path.Combine(userGraceDir, \"userconfig.json\")\n\n    let ensureUserGraceDirectory () =\n        let userGraceDir = getUserGraceDirectory ()\n        Directory.CreateDirectory(userGraceDir) |> ignore\n        userGraceDir\n\n    let saveUserConfiguration (configuration: UserConfiguration) =\n        try\n            let path = getUserConfigurationPath ()\n            ensureUserGraceDirectory () |> ignore\n            let json = serialize (normalizeConfiguration configuration)\n            File.WriteAllText(path, json)\n            Ok()\n        with\n        | ex -> Error $\"Failed to write user configuration: {ex.Message}\"\n\n    let loadUserConfiguration () =\n        let defaultConfiguration = UserConfiguration()\n\n        try\n            ensureUserGraceDirectory () |> ignore\n            let path = getUserConfigurationPath ()\n\n            if not <| File.Exists(path) then\n                let json = serialize defaultConfiguration\n                File.WriteAllText(path, json)\n\n                { Configuration = defaultConfiguration; WasCorrupt = false; ErrorMessage = None; CreatedNew = true }\n            else\n                let json = File.ReadAllText(path)\n\n                let configuration = JsonSerializer.Deserialize<UserConfiguration>(json, Constants.JsonSerializerOptions)\n\n                if obj.ReferenceEquals(configuration, null) then\n                    {\n                        Configuration = defaultConfiguration\n                        WasCorrupt = true\n                        ErrorMessage = Some \"User configuration file is empty or invalid JSON.\"\n                        CreatedNew = false\n                    }\n                else\n                    { Configuration = normalizeConfiguration configuration; WasCorrupt = false; ErrorMessage = None; CreatedNew = false }\n        with\n        | ex ->\n            {\n                Configuration = defaultConfiguration\n                WasCorrupt = true\n                ErrorMessage = Some $\"Failed to read user configuration: {ex.Message}\"\n                CreatedNew = false\n            }\n"
  },
  {
    "path": "src/Grace.Shared/Combinators.fs",
    "content": "﻿namespace Grace.Shared\n\n///// ===========================================\n///// Common types and functions shared across multiple projects\n///// ===========================================\nmodule CombinatorExamples =\n    let x = 0\n\n//    // the two-track type\n//    type Result<'TSuccess,'TFailure> =\n//        | Success of 'TSuccess\n//        | Failure of 'TFailure\n\n//    // convert a single value into a two-track result\n//    let succeed x =\n//        Success x\n\n//    // convert a single value into a two-track result\n//    let fail x =\n//        Failure x\n\n//    // apply either a success function or failure function\n//    let either successFunc failureFunc twoTrackInput =\n//        match twoTrackInput with\n//        | Success s -> successFunc s\n//        | Failure f -> failureFunc f\n\n//// convert a switch function into a two-track function\n//let bind f =\n//    either f fail\n\n//    // pipe a two-track value into a switch function\n//    let (>>=) x f =\n//        bind f x\n\n//    // compose two switches into another switch\n//    let (>=>) s1 s2 =\n//        s1 >> bind s2\n\n//    // convert a one-track function into a switch\n//    let switch f =\n//        f >> succeed\n\n//    // convert a one-track function into a two-track function\n//    let map f =\n//        either (f >> succeed) fail\n\n//    // convert a dead-end function into a one-track function\n//    let tee f x =\n//        f x; x\n\n//    // convert a one-track function into a switch with exception handling\n//    let tryCatch f exnHandler x =\n//        try\n//            f x |> succeed\n//        with\n//        | ex -> exnHandler ex |> fail\n\n//    // convert two one-track functions into a two-track function\n//    let doubleMap successFunc failureFunc =\n//        either (successFunc >> succeed) (failureFunc >> fail)\n\n//    // add two switches in parallel\n//    let plus addSuccess addFailure switch1 switch2 x =\n//        match (switch1 x),(switch2 x) with\n//        | Success s1,Success s2 -> Success (addSuccess s1 s2)\n//        | Failure f1,Success _  -> Failure f1\n//        | Success _ ,Failure f2 -> Failure f2\n//        | Failure f1,Failure f2 -> Failure (addFailure f1 f2)\n"
  },
  {
    "path": "src/Grace.Shared/Constants.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen NodaTime\nopen MessagePack\nopen MessagePack.FSharp\nopen MessagePack.NodaTime\nopen MessagePack.Resolvers\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.FSharp.Reflection\nopen NodaTime.Serialization.SystemTextJson\nopen Polly\nopen Polly.Contrib.WaitAndRetry\nopen System\nopen System.IO\nopen System.Text.Encodings.Web\nopen System.Text.Json\nopen System.Text.Json.Serialization\nopen System.Text.RegularExpressions\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\nmodule Constants =\n\n    /// The universal serialization options for F#-specific data types in Grace.\n    ///\n    /// See https://github.com/Tarmil/FSharp.SystemTextJson/blob/master/docs/Customizing.md for more information about these options.\n    let private jsonFSharpOptions =\n        JsonFSharpOptions\n            .Default()\n            .WithAllowNullFields(true)\n            .WithUnionFieldsName(\"value\")\n            .WithUnionTagNamingPolicy(JsonNamingPolicy.CamelCase)\n            .WithUnionTagCaseInsensitive(true)\n            .WithUnionEncoding(\n                JsonUnionEncoding.ExternalTag\n                ||| JsonUnionEncoding.UnwrapFieldlessTags\n                ||| JsonUnionEncoding.UnwrapSingleFieldCases\n                ||| JsonUnionEncoding.UnwrapSingleCaseUnions\n                ||| JsonUnionEncoding.NamedFields\n            )\n            .WithUnwrapOption(true)\n\n    /// The universal JSON serialization options for Grace.\n    let public JsonSerializerOptions = JsonSerializerOptions()\n    JsonSerializerOptions.Converters.Add(JsonFSharpConverter(jsonFSharpOptions))\n    JsonSerializerOptions.Converters.Add(JsonStringEnumConverter(JsonNamingPolicy.CamelCase))\n    JsonSerializerOptions.AllowTrailingCommas <- true\n    JsonSerializerOptions.DefaultBufferSize <- 64 * 1024\n    JsonSerializerOptions.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingDefault // JsonSerializerOptions.IgnoreNullValues is deprecated. This is the new way to say it.\n    JsonSerializerOptions.IncludeFields <- true // Include fields in serialization. This is important for F# records and discriminated unions.\n    JsonSerializerOptions.IndentSize <- 2\n    JsonSerializerOptions.MaxDepth <- 64 // Default is 64, and I'm assuming this setting would need to change if there were a directory depth greater than 64 in a repo.\n    JsonSerializerOptions.NumberHandling <- JsonNumberHandling.AllowReadingFromString\n    JsonSerializerOptions.PropertyNameCaseInsensitive <- true // Case sensitivity is from the 1970's. We should let it go.\n    //JsonSerializerOptions.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase\n    JsonSerializerOptions.ReadCommentHandling <- JsonCommentHandling.Skip\n    JsonSerializerOptions.ReferenceHandler <- ReferenceHandler.IgnoreCycles\n    JsonSerializerOptions.RespectNullableAnnotations <- true\n    JsonSerializerOptions.UnknownTypeHandling <- JsonUnknownTypeHandling.JsonElement\n    JsonSerializerOptions.WriteIndented <- true\n\n    JsonSerializerOptions.ConfigureForNodaTime(NodaTime.DateTimeZoneProviders.Tzdb)\n    |> ignore\n\n    /// The universal MessagePack serialization options for Grace.\n    let messagePackSerializerOptions =\n        MessagePackSerializerOptions\n            .Standard\n            .WithResolver(\n                CompositeResolver.Create(\n                    [|\n                        // 1) Generated formatters (classes like Grace_Types_Types.DirectoryVersionFormatter1)\n                        //GeneratedResolver.Instance\n                        // 2) F# helpers for records/DUs\n                        FSharpResolver.Instance\n                        // 3) NodaTime formatters (Instant, LocalDate, etc.)\n                        NodatimeResolver.Instance\n                        // 4) Final fallback\n                        StandardResolver.Instance\n                    |]\n                )\n            )\n            .WithCompression(MessagePackCompression.Lz4BlockArray)\n            .WithSecurity(MessagePackSecurity.UntrustedData)\n\n    /// Attempts to locate the union type from a runtime instance, even when the\n    /// value is represented by the compiler-generated nested case type.\n    let private tryGetUnionType (runtimeType: Type) =\n        let rec loop currentType =\n            if isNull currentType then None\n            elif FSharpType.IsUnion currentType then Some currentType\n            else loop currentType.DeclaringType\n\n        loop runtimeType\n\n    /// Converts both the type name and case name of a discriminated union to a string.\n    ///\n    /// Example: Animal.Dog -> \"Animal.Dog\"\n    let getDiscriminatedUnionFullName (x: 'T) =\n        let runtimeType = x.GetType()\n\n        match tryGetUnionType runtimeType with\n        | Some unionType ->\n            let (case, _) = FSharpValue.GetUnionFields(x, unionType)\n            $\"{unionType.Name}.{case.Name}\"\n        | None -> runtimeType.Name\n\n    /// Converts just the case name of a discriminated union to a string.\n    ///\n    /// Example: Animal.Dog -> \"Dog\"\n    let getDiscriminatedUnionCaseName (x: 'T) =\n        let runtimeType = x.GetType()\n\n        match tryGetUnionType runtimeType with\n        | Some unionType ->\n            let (case, _) = FSharpValue.GetUnionFields(x, unionType)\n            $\"{case.Name}\"\n        | None -> runtimeType.Name\n\n    /// The name of the Grace System user.\n    [<Literal>]\n    let GraceSystemUser = \"gracesystem\"\n\n    /// The name of the storage service for Actor storage. This should be a document database.\n    [<Literal>]\n    let GraceActorStorage = \"actorstorage\"\n\n    /// The name of the storage service for in-memory actors.\n    [<Literal>]\n    let GraceInMemoryStorage = \"in-memory\"\n\n    /// The name of the service for Grace object storage.\n    [<Literal>]\n    let GraceDiffStorage = \"gracediffstorage\"\n\n    /// The name of the service for Grace event pub/sub.\n    [<Literal>]\n    let GracePubSubService = \"graceevents\"\n\n    /// The name of the Orleans stream provider to publish to.\n    [<Literal>]\n    let GraceEventStreamProvider = \"graceeventstreamprovider\"\n\n    /// The name of the event topic to publish to.\n    [<Literal>]\n    let GraceEventStreamTopic = \"graceeventstream\"\n\n    /// The name of the directory that holds Grace information in a repository.\n    [<Literal>]\n    let GraceConfigDirectory = \".grace\"\n\n    /// The name of the directory that holds locally-cached files in a repository.\n    [<Literal>]\n    let GraceObjectsDirectory = \"objects\"\n\n    /// The name of Grace's configuration file.\n    [<Literal>]\n    let GraceConfigFileName = \"graceconfig.json\"\n\n    /// The directory name of Grace's DirectoryVersion cache directory.\n    [<Literal>]\n    let GraceDirectoryVersionCacheName = \"directoryVersions\"\n\n    /// The folder name to use in object storage for directory version contents .zip files.\n    [<Literal>]\n    let GraceZipFilesFolderName = \"Grace-ZipFiles\"\n\n    /// The name of the file that holds the file specifications to ignore.\n    [<Literal>]\n    let GraceIgnoreFileName = \".graceignore\"\n\n    /// The name of the file that holds the current local index for Grace.\n    [<Literal>]\n    let GraceLocalStateDbFileName = \"grace-local.db\"\n\n    /// The name of the file that holds the current local index for Grace.\n    [<Literal>]\n    let GraceObjectCacheFile = GraceLocalStateDbFileName\n\n    /// The default branch name for new repositories.\n    [<Literal>]\n    let InitialBranchName = \"main\"\n\n    /// The configuration version number used by this release of Grace.\n    let CurrentConfigurationVersion = \"0.1\"\n\n    /// The configuration version number used by this release of Grace.\n    [<Literal>]\n    let ServerApiVersionHeaderKey = \"X-Api-Version\"\n\n    /// Environment variables used by Grace.\n    module EnvironmentVariables =\n        /// The environment variable that contains the Application Insights connection string.\n        [<Literal>]\n        let ApplicationInsightsConnectionString = \"grace__applicationinsightsconnectionstring\"\n\n        /// The environment variable that contains the Azure Cosmos DB Connection String.\n        [<Literal>]\n        let AzureCosmosDBConnectionString = \"grace__azurecosmosdb__connectionstring\"\n\n        /// The environment variable that contains the name of the CosmosDB container to use for Grace.\n        [<Literal>]\n        let AzureCosmosDBContainerName = \"grace__azurecosmosdb__container_name\"\n\n        /// The environment variable that contains the name of the CosmosDB database to use for Grace.\n        [<Literal>]\n        let AzureCosmosDBDatabaseName = \"grace__azurecosmosdb__database_name\"\n\n        /// The environment variable that exposes the Cosmos DB endpoint when using managed identity.\n        [<Literal>]\n        let AzureCosmosDBEndpoint = \"grace__azurecosmosdb__endpoint\"\n\n        /// The environment variable that contains the Azure Storage Connection String.\n        [<Literal>]\n        let AzureStorageConnectionString = \"grace__azure_storage__connectionstring\"\n\n        /// The environment variable that overrides the Azure Storage account name used for managed identity.\n        [<Literal>]\n        let AzureStorageAccountName = \"grace__azure_storage__account_name\"\n\n        /// The environment variable that overrides the Azure Storage endpoint suffix (default: core.windows.net).\n        [<Literal>]\n        let AzureStorageEndpointSuffix = \"grace__azure_storage__endpoint_suffix\"\n\n        /// The environment variable that contains the Azure Storage Key.\n        [<Literal>]\n        let AzureStorageKey = \"grace__azure_storage__key\"\n\n        /// The environment variable that contains the Grace server Uri. The Uri MUST include a port number,\n        /// and MUST NOT include a trailing slash.\n        [<Literal>]\n        let GraceServerUri = \"GRACE_SERVER_URI\"\n\n        /// The environment variable that contains a Grace personal access token\n        /// for non-interactive authentication.\n        [<Literal>]\n        let GraceToken = \"GRACE_TOKEN\"\n\n        /// The environment variable that overrides the local token file path.\n        [<Literal>]\n        let GraceTokenFile = \"GRACE_TOKEN_FILE\"\n\n        /// External authentication settings for Auth0 / OIDC.\n        [<Literal>]\n        let GraceAuthOidcAuthority = \"grace__auth__oidc__authority\"\n\n        /// External authentication settings for Auth0 / OIDC.\n        [<Literal>]\n        let GraceAuthOidcAudience = \"grace__auth__oidc__audience\"\n\n        /// External authentication settings for Auth0 / OIDC CLI application.\n        [<Literal>]\n        let GraceAuthOidcCliClientId = \"grace__auth__oidc__cli_client_id\"\n\n        /// External authentication settings for Auth0 / OIDC CLI application.\n        [<Literal>]\n        let GraceAuthOidcCliRedirectPort = \"grace__auth__oidc__cli_redirect_port\"\n\n        /// External authentication settings for Auth0 / OIDC CLI application.\n        [<Literal>]\n        let GraceAuthOidcCliScopes = \"grace__auth__oidc__cli_scopes\"\n\n        /// External authentication settings for Auth0 / OIDC machine-to-machine auth.\n        [<Literal>]\n        let GraceAuthOidcM2mClientId = \"grace__auth__oidc__m2m_client_id\"\n\n        /// External authentication settings for Auth0 / OIDC machine-to-machine auth.\n        [<Literal>]\n        let GraceAuthOidcM2mClientSecret = \"grace__auth__oidc__m2m_client_secret\"\n\n        /// External authentication settings for Auth0 / OIDC machine-to-machine auth.\n        [<Literal>]\n        let GraceAuthOidcM2mScopes = \"grace__auth__oidc__m2m_scopes\"\n\n        /// Deprecated: External authentication settings for Microsoft (MSA + Entra).\n        [<Literal>]\n        let GraceAuthMicrosoftClientId = \"grace__auth__microsoft__client_id\"\n\n        /// Deprecated: External authentication settings for Microsoft (MSA + Entra).\n        [<Literal>]\n        let GraceAuthMicrosoftClientSecret = \"grace__auth__microsoft__client_secret\"\n\n        /// Deprecated: External authentication settings for Microsoft (MSA + Entra).\n        [<Literal>]\n        let GraceAuthMicrosoftTenantId = \"grace__auth__microsoft__tenant_id\"\n\n        /// Deprecated: External authentication settings for Microsoft (MSA + Entra).\n        [<Literal>]\n        let GraceAuthMicrosoftAuthority = \"grace__auth__microsoft__authority\"\n\n        /// Deprecated: External authentication settings for Microsoft (MSA + Entra).\n        [<Literal>]\n        let GraceAuthMicrosoftApiScope = \"grace__auth__microsoft__api_scope\"\n\n        /// Deprecated: External authentication settings for Microsoft (MSA + Entra).\n        [<Literal>]\n        let GraceAuthMicrosoftCliClientId = \"grace__auth__microsoft__cli_client_id\"\n\n        /// Default PAT lifetime in days.\n        [<Literal>]\n        let GraceAuthPatDefaultLifetimeDays = \"grace__auth__pat__default_lifetime_days\"\n\n        /// Maximum PAT lifetime in days.\n        [<Literal>]\n        let GraceAuthPatMaxLifetimeDays = \"grace__auth__pat__max_lifetime_days\"\n\n        /// Allows PATs with no expiry when set to true.\n        [<Literal>]\n        let GraceAuthPatAllowNoExpiry = \"grace__auth__pat__allow_no_expiry\"\n\n        /// Semicolon-delimited list of user principals that should be bootstrapped as SystemAdmin.\n        [<Literal>]\n        let GraceAuthzBootstrapSystemAdminUsers = \"grace__authz__bootstrap__system_admin_users\"\n\n        /// Semicolon-delimited list of group principals that should be bootstrapped as SystemAdmin.\n        [<Literal>]\n        let GraceAuthzBootstrapSystemAdminGroups = \"grace__authz__bootstrap__system_admin_groups\"\n\n        /// The name of the container in object storage that holds memoized RecursiveDirectoryVersions.\n        [<Literal>]\n        let DirectoryVersionContainerName = \"grace__azure_storage__directoryversion_container_name\"\n\n        /// The name of the container in object storage that holds cached Diff contents.\n        [<Literal>]\n        let DiffContainerName = \"grace__azure_storage__diff_container_name\"\n\n        /// The name of the container in object storage that holds memoized RecursiveDirectoryVersions.\n        [<Literal>]\n        let ZipFileContainerName = \"grace__azure_storage__zipfile_container_name\"\n\n        /// The environment variable that contains the maximum number of reminders that each Grace instance should retrieve from the database and publish for processing.\n        [<Literal>]\n        let GraceReminderBatchSize = \"grace__reminder__batch__size\"\n\n        /// The environment variable that contains the name of the Orleans cluster to use.\n        [<Literal>]\n        let OrleansClusterId = \"grace__orleans__clusterid\"\n\n        /// The environment variable that contains the name of the Orleans service to use.\n        [<Literal>]\n        let OrleansServiceId = \"grace__orleans__serviceid\"\n\n        /// The environment variable that contains the Redis host name.\n        [<Literal>]\n        let RedisHost = \"grace__redis__host\"\n\n        /// The environment variable that contains the Redis port number.\n        [<Literal>]\n        let RedisPort = \"grace__redis__port\"\n\n        /// The environment variable that selects the pub-sub provider.\n        [<Literal>]\n        let GracePubSubSystem = \"grace__pubsub__system\"\n\n        /// Allows anonymous access to the Prometheus scraping endpoint when set to true.\n        [<Literal>]\n        let GraceMetricsAllowAnonymous = \"grace__metrics__allow_anonymous\"\n\n        /// Azure Service Bus connection string\n        [<Literal>]\n        let AzureServiceBusConnectionString = \"grace__azure_service_bus__connectionstring\"\n\n        /// Azure Service Bus fully qualified namespace (e.g., sb://foo.servicebus.windows.net ).\n        [<Literal>]\n        let AzureServiceBusNamespace = \"grace__azure_service_bus__namespace\"\n\n        /// Azure Service Bus topic name for Grace events.\n        [<Literal>]\n        let AzureServiceBusTopic = \"grace__azure_service_bus__topic\"\n\n        /// Azure Service Bus subscription name for Grace events.\n        [<Literal>]\n        let AzureServiceBusSubscription = \"grace__azure_service_bus__subscription\"\n\n        /// AWS SQS queue URL placeholder for future support.\n        [<Literal>]\n        let AwsSqsQueueUrl = \"grace__aws_sqs__queue_url\"\n\n        /// AWS region for SQS (future use).\n        [<Literal>]\n        let AwsRegion = \"grace__aws_sqs__region\"\n\n        /// The environment variable that contains the debug environment flag.\n        [<Literal>]\n        let DebugEnvironment = \"grace__debug_environment\"\n\n        /// Google Cloud Pub/Sub project identifier (future use).\n        [<Literal>]\n        let GooglePubSubProjectId = \"grace__gcp__projectid\"\n\n        /// Google Cloud Pub/Sub topic name (future use).\n        [<Literal>]\n        let GooglePubSubTopic = \"grace__gcp__topic\"\n\n        /// Google Cloud Pub/Sub subscription name (future use).\n        [<Literal>]\n        let GooglePubSubSubscription = \"grace__gcp__subscription\"\n\n        /// The environment variable that contains the directory for Grace Server log files.\n        [<Literal>]\n        let GraceLogDirectory = \"grace__log_directory\"\n\n    /// The default CacheControl header for object storage.\n    [<Literal>]\n    let BlobCacheControl = \"public,max-age=86400,no-transform\"\n\n    /// The expiration time for a Shared Access Signature token, in minutes.\n    let SharedAccessSignatureExpiration = 15.0\n\n    /// The default maximum number of reminders to return in list queries.\n    [<Literal>]\n    let DefaultReminderMaxCount = 100\n\n    /// The path that indicates the root directory of the repository.\n    [<Literal>]\n    let RootDirectoryPath = \".\"\n\n    /// The key for the HttpContext metadata value that holds the CorrelationId for this transaction.\n    [<Literal>]\n    let CorrelationId = \"correlationId\"\n\n    /// The property name for the CurrentCommand being handled by a grain.\n    [<Literal>]\n    let CurrentCommandProperty = \"currentCommand\"\n\n    /// The property name for the ActorName being handled by a grain.\n    [<Literal>]\n    let ActorNameProperty = \"actorName\"\n\n    /// The header name for a W3C trace.\n    [<Literal>]\n    let Traceparent = \"traceparent\"\n\n    /// The header name for W3C trace state.\n    [<Literal>]\n    let Tracestate = \"tracestate\"\n\n    /// The key for the HttpRequest and HttpResponse header that holds the CorrelationId for this transaction.\n    [<Literal>]\n    let CorrelationIdHeaderKey = \"X-Correlation-Id\"\n\n    /// <summary>\n    /// Validates that a string is a valid Grace object name.\n    ///\n    /// Regex: ^[A-Za-z][A-Za-z0-9\\-]{1,63}$\n    ///\n    /// A valid object name in Grace has between 2 and 64 characters, has a letter for the first character ([A-Za-z]), and letters, numbers, or a dash (-) for the rest ([A-Za-z0-9\\-_]{1,63}).\n    ///\n    /// See https://regexper.com for a diagram.\n    /// </summary>\n    let GraceNameRegex =\n        new Regex(\n            \"^[A-Za-z][A-Za-z0-9\\-]{1,63}$\",\n            RegexOptions.CultureInvariant\n            ||| RegexOptions.Compiled,\n            TimeSpan.FromSeconds(1.0)\n        )\n    // Note: The timeout value of 1s is a crazy big maximum time; matching against this should take less than 1ms.\n\n    /// Validates that a string is a full or partial valid SHA-256 hash value, between 2 and 64 hexadecimal characters.\n    ///\n    /// Regex: ^[0-9a-fA-F]{2,64}$\n    let Sha256Regex =\n        new Regex(\n            \"^[0-9a-fA-F]{2,64}$\",\n            RegexOptions.CultureInvariant\n            ||| RegexOptions.Compiled,\n            TimeSpan.FromSeconds(1.0)\n        )\n\n    /// The backoff policy used by Grace for server requests.\n    let private backoffWithJitter = Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay = (TimeSpan.FromSeconds(0.25)), retryCount = 7, fastFirst = false)\n\n    /// An exponential retry policy, with backoffs starting at 0.25s, and retrying 8 times.\n    let DefaultRetryPolicy =\n        Policy\n            .Handle<Exception>(fun ex -> ex.GetType() <> typeof<KeyNotFoundException>)\n            .WaitAndRetry(backoffWithJitter)\n\n    /// An exponential retry policy, with backoffs starting at 0.25s, and retrying 8 times.\n    let DefaultAsyncRetryPolicy =\n        Policy\n            .Handle<Exception>(fun ex -> ex.GetType() <> typeof<KeyNotFoundException>)\n            .WaitAndRetryAsync(backoffWithJitter)\n\n    let private fileCopyBackoff = Backoff.LinearBackoff(initialDelay = (TimeSpan.FromSeconds(1.0)), retryCount = 16, factor = 1.5, fastFirst = false)\n\n    /// A linear retry policy for copying files locally, with backoffs starting at 1s and retrying 16 times.\n    // This retry policy helps with large files. `grace watch` will see that the file is arriving, but if that file takes longer to be written than the next tick,\n    // we get an IOException when we try to compute the Sha256Hash and copy it to the object directory. This policy allows us to wait until the file is complete.\n    let DefaultFileCopyRetryPolicy =\n        Policy\n            .Handle<IOException>(fun ex -> ex.GetType() <> typeof<KeyNotFoundException>)\n            .WaitAndRetry(fileCopyBackoff)\n\n    /// Grace's global settings for Parallel.ForEach/ForEachAsync expressions; sets MaxDegreeofParallelism to maximize performance.\n    // I'm choosing a higher-than-usual number here because these parallel loops are used in code where most of the time is spent on network\n    //   and disk traffic - and therefore Task<'T> - and we can run lots of them simultaneously.\n    let ParallelOptions = ParallelOptions(MaxDegreeOfParallelism = Environment.ProcessorCount * 1)\n\n    /// Default directory size magic value.\n    let InitialDirectorySize = int64 -1\n\n    /// The default root branch Id for a repository. Value: 38EC9A98-00B0-4FA3-8CC5-ACFB04E445A7.\n    let DefaultParentBranchId = Guid(\"38EC9A98-00B0-4FA3-8CC5-ACFB04E445A7\") // There's nothing special about this Guid. I just generated it one day.\n\n    /// A special Grace Event Actor Id used for publishing events in Grace.Server. Value: 63097EF4-9D67-4CFB-8010-938484668E4A.\n    let GraceEventActorId = Guid(\"63097EF4-9D67-4CFB-8010-938484668E4A\") // There's nothing special about this Guid. I just generated it one day.\n\n    /// The name of the inter-process communication file used by grace watch to share status with other invocations of Grace.\n    [<Literal>]\n    let IpcFileName = \"graceWatchStatus.json\"\n\n    /// The name of the file to let `grace watch` know that `grace rebase` or `grace switch` is underway.\n    [<Literal>]\n    let UpdateInProgressFileName = \"graceUpdateInProgress.txt\"\n\n    /// The custom alphabet to use when generating a NanoId for a CorrelationId. This alphabet is URL-safe. Consists of \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._-\".\n    [<Literal>]\n    let CorrelationIdAlphabet = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._-\"\n\n    /// The default value for a timestamp during record construction. Equal to 2000-01-01T00:00:00Z.\n    let DefaultTimestamp = Instant.FromDateTimeUtc(DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc))\n\n    /// The default name of the account in object storage that holds Grace objects.\n    [<Literal>]\n    let DefaultObjectStorageAccount = \"gracevcsdevelopment\"\n\n    /// Values used with Grace's MemoryCache.\n    module MemoryCache =\n        /// A special Guid value that means \"false\" or \"we know there's no value here\". Used in places where the cache entry value is a Guid.\n        let EntityDoesNotExistGuid = Guid(\"27F21D8A-DA1D-4C73-8773-4AA5A5712612\") // There's nothing special about this Guid. I just generated it one day.\n\n        /// A special Guid value that means \"false\" or \"we know there's no value here\". Used in places where the cache entry value is a Guid.\n        let EntityDoesNotExist = box EntityDoesNotExistGuid\n\n        /// The default value to store in Grace's MemoryCache when an entity is known to exist. This is a one-character string because MemoryCache values are Objects, and a const string doesn't require boxing.\n        [<Literal>]\n        let Exists = \"y\"\n\n        /// The default value to store in Grace's MemoryCache when a value is known not to exist. This is a one-character string because MemoryCache values are Objects, and a const string doesn't require boxing.\n        [<Literal>]\n        let DoesNotExist = \"n\"\n\n        /// The key name for Grace's configuration in the MemoryCache.\n        [<Literal>]\n        let GraceConfiguration = \"GraceConfiguration\"\n\n        /// The default expiration time for a memory cache entry, in minutes.\n#if DEBUG\n        let DefaultExpirationTime = TimeSpan.FromMinutes(2.0)\n#else\n        let DefaultExpirationTime = TimeSpan.FromMinutes(2.0)\n#endif\n\n/// A MemoryCacheEntryOptions object that uses Grace's default expiration time.\n//let DefaultMemoryCacheEntryOptions = MemoryCacheEntryOptions().SetAbsoluteExpiration(DefaultExpirationTime)\n\nmodule Results =\n    let Ok = 0\n    let Exception = -1\n    let FileNotFound = -2\n    let ConfigurationFileNotFound = -3\n    let InvalidConfigurationFile = -4\n    let NotParsed = -98\n    let CommandNotFound = -99\n    let ThisShouldNeverHappen = -999\n"
  },
  {
    "path": "src/Grace.Shared/Converters/BranchDtoConverter.Shared.fs",
    "content": "namespace Grace.Shared.Converters\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Branch\nopen System\nopen System.Collections.Generic\nopen System.Text.Json\nopen System.Text.Json.Serialization\n\ntype BranchDtoConverter() =\n    inherit JsonConverter<BranchDto>()\n\n    override this.Read((reader: byref<Utf8JsonReader>), (typeToConvert: Type), (jsonSerializerOptions: JsonSerializerOptions)) : BranchDto =\n        let dictionary = Dictionary<string, string>()\n        let mutable branchDto = BranchDto.Default\n\n        let t = typeof<BranchDto>\n        let fields = t.GetFields()\n        let properties = t.GetProperties()\n        let members = t.GetMembers()\n\n        logToConsole $\"\"\"{sprintf \"%A\" fields}\"\"\"\n        logToConsole $\"\"\"{sprintf \"%A\" properties}\"\"\"\n        logToConsole $\"\"\"{sprintf \"%A\" members}\"\"\"\n\n        if reader.TokenType <> JsonTokenType.StartObject then\n            raise (JsonException(\"Invalid JSON text.\"))\n        else\n            while reader.Read() do\n                if reader.TokenType = JsonTokenType.EndObject then\n                    ()\n                elif reader.TokenType <> JsonTokenType.PropertyName then\n                    ()\n                else\n                    let key = reader.GetString()\n                    ()\n\n                    reader.Read() |> ignore\n                    let value: string = reader.GetString()\n                    dictionary.Add(key, value)\n\n            for kvp in dictionary do\n                logToConsole $\"{kvp.Key}: {kvp.Value}\"\n\n            branchDto\n\n    override this.Write((writer: Utf8JsonWriter), (value: BranchDto), (jsonSerializerOptions: JsonSerializerOptions)) = ()\n"
  },
  {
    "path": "src/Grace.Shared/Diff.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen DiffPlex\nopen DiffPlex.Chunkers\nopen DiffPlex.DiffBuilder.Model\nopen Grace.Shared\nopen Grace.Types\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.IO\nopen System.Linq\nopen System.Text\nopen System.Threading.Tasks\nopen System.IO.Compression\n\nmodule Diff =\n\n    let lineChunker = Chunkers.LineChunker()\n    let wordChunker = Chunkers.WordChunker()\n\n    /// Captures sections of file changes, with prefixed and suffixed unchanged lines for context.\n    let processDiffModel (diffLines: List<DiffPiece>) (includeImaginary: bool) =\n        let mutable mostRecentUnchangedLines = 0\n        let mutable changeIsInProcess = false\n        let diffList = List<DiffPiece>()\n        let diffSections = List<DiffPiece []>()\n\n        for i = 0 to diffLines.Count - 1 do\n            let diffLine = diffLines[i]\n\n            // If we have two consecutive unchanged lines, and we're already in the middle of a change, finish out that change\n            //  by adding those two unchanged lines.\n            if diffLine.Type = ChangeType.Unchanged\n               && (i < diffLines.Count - 1)\n               && (diffLines[i + 1].Type = ChangeType.Unchanged) then\n                mostRecentUnchangedLines <- i\n\n                if changeIsInProcess then\n                    // We've hit the first spot with unchanged lines after dealing with changes, so write the\n                    // unchanged lines and finish this section by saving it in DiffSections\n                    diffList.Add(diffLines[mostRecentUnchangedLines])\n                    diffList.Add(diffLines[mostRecentUnchangedLines + 1])\n                    changeIsInProcess <- false\n                    diffSections.Add(diffList.ToArray())\n                    diffList.Clear()\n\n            // We have changes to process.\n            elif diffLine.Type = ChangeType.Deleted\n                 || diffLine.Type = ChangeType.Inserted\n                 || diffLine.Type = ChangeType.Modified\n                 || (diffLine.Type = ChangeType.Imaginary\n                     && includeImaginary) then\n                if not <| changeIsInProcess then\n                    // We're starting a new diff section, so flip the flag and write the most recent\n                    // unchanged lines to start the section.\n                    changeIsInProcess <- true\n\n                    if mostRecentUnchangedLines > 0 then\n                        diffList.Add(diffLines[mostRecentUnchangedLines])\n                        diffList.Add(diffLines[mostRecentUnchangedLines + 1])\n                // Write this change to the list.\n                diffList.Add(diffLine)\n\n        // Capture changes in the last couple of lines, if any.\n        if changeIsInProcess then\n            if diffLines[diffLines.Count - 1].Type = ChangeType.Unchanged then\n                diffList.Add(diffLines[diffLines.Count - 1])\n\n            diffSections.Add(diffList.ToArray())\n\n        diffSections\n\n    /// Generates the inline and side-by-side diffs for two files.\n    ///\n    /// NOTE: The input streams are expected to be uncompressed.\n    let diffTwoFiles (fileStream1: Stream) (fileStream2: Stream) =\n        task {\n            use textReader1 = new StreamReader(fileStream1)\n            let! fileContents1 = textReader1.ReadToEndAsync()\n            do! fileStream1.DisposeAsync()\n\n            use textReader2 = new StreamReader(fileStream2)\n            let! fileContents2 = textReader2.ReadToEndAsync()\n            do! fileStream2.DisposeAsync()\n\n            let inlineDiff = DiffPlex.DiffBuilder.InlineDiffBuilder(Differ.Instance)\n\n            let inlineDiffPaneModel =\n                inlineDiff.BuildDiffModel(fileContents1, fileContents2, ignoreWhitespace = true, ignoreCase = false, chunker = lineChunker)\n\n            let sideBySideDiff = DiffBuilder.SideBySideDiffBuilder(Differ.Instance, lineChunker, wordChunker)\n\n            let sideBySideDiffModel = sideBySideDiff.BuildDiffModel(fileContents1, fileContents2, ignoreWhitespace = true, ignoreCase = false)\n\n            let inlineDiffSections =\n                if inlineDiffPaneModel.HasDifferences then\n                    let diffLines = inlineDiffPaneModel.Lines\n                    processDiffModel diffLines false\n                else\n                    List<DiffPiece []>()\n\n            let sideBySideOldSections =\n                if sideBySideDiffModel.OldText.HasDifferences then\n                    let diffLines = sideBySideDiffModel.OldText.Lines\n                    processDiffModel diffLines true\n                else\n                    List<DiffPiece []>()\n\n            let sideBySideNewSections =\n                if sideBySideDiffModel.NewText.HasDifferences then\n                    let diffLines = sideBySideDiffModel.NewText.Lines\n                    processDiffModel diffLines true\n                else\n                    List<DiffPiece []>()\n\n            return {| InlineDiff = inlineDiffSections; SideBySideOld = sideBySideOldSections; SideBySideNew = sideBySideNewSections |}\n        }\n"
  },
  {
    "path": "src/Grace.Shared/Dto/Dto.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen System\nopen System.Collections.Generic\nopen System.Runtime.Serialization\nopen Orleans\n\nmodule Dto =\n\n    let x = 1\n"
  },
  {
    "path": "src/Grace.Shared/Evidence.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen DiffPlex.DiffBuilder.Model\nopen Grace.Shared.Utilities\nopen Grace.Types.Diff\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen System\nopen System.Collections.Generic\nopen System.Linq\nopen System.Text\nopen System.Text.RegularExpressions\n\nmodule Evidence =\n    let private estimateTokens (byteCount: int) = if byteCount <= 0 then 0 else max 1 (byteCount / 4)\n\n    let private redact (patterns: string list) (content: string) =\n        let redacted =\n            (content, patterns)\n            ||> List.fold (fun current pattern ->\n                if String.IsNullOrWhiteSpace pattern then\n                    current\n                else\n                    Regex.Replace(current, pattern, \"***REDACTED***\"))\n\n        redacted, not (String.Equals(content, redacted, StringComparison.Ordinal))\n\n    let private getLineBounds (lines: DiffPiece array) =\n        let positions =\n            lines\n            |> Seq.choose (fun piece -> if piece.Position.HasValue then Some piece.Position.Value else None)\n            |> Seq.toList\n\n        match positions with\n        | [] ->\n            let lineCount = max 1 lines.Length\n            1, lineCount\n        | _ -> positions |> List.min, positions |> List.max\n\n    let private scoreReasons (riskProfile: DeterministicRiskProfile option) (relativePath: RelativePath) (lines: DiffPiece array) =\n        let changedLineCount =\n            lines\n            |> Array.filter (fun piece -> piece.Type <> ChangeType.Unchanged)\n            |> Array.length\n\n        let mutable reasons = List<EvidenceScoreReason>()\n\n        if changedLineCount > 0 then\n            reasons.Add({ Feature = \"ChangeMagnitude\"; Score = float changedLineCount })\n\n        match riskProfile with\n        | Some profile ->\n            if profile.SensitivePathsTouched\n               |> List.contains relativePath then\n                reasons.Add({ Feature = \"SensitivePath\"; Score = 10.0 })\n\n            if profile.DependencyConfigChanges\n               |> List.contains relativePath then\n                reasons.Add({ Feature = \"DependencyConfig\"; Score = 5.0 })\n\n            if profile.ApiSurfaceSignals\n               |> List.contains relativePath then\n                reasons.Add({ Feature = \"ApiSurfaceSignal\"; Score = 5.0 })\n        | None -> ()\n\n        let score = reasons |> Seq.sumBy (fun reason -> reason.Score)\n        score, reasons |> Seq.toList\n\n    let private topReasons (sliceSummaries: EvidenceSliceSummary list) =\n        sliceSummaries\n        |> Seq.collect (fun summary -> summary.Reasons)\n        |> Seq.groupBy (fun reason -> reason.Feature)\n        |> Seq.map (fun (feature, reasons) -> { Feature = feature; Score = reasons |> Seq.sumBy (fun reason -> reason.Score) })\n        |> Seq.sortByDescending (fun reason -> reason.Score)\n        |> Seq.truncate 3\n        |> Seq.toList\n\n    let buildEvidenceSet\n        (stage: EvidenceStage)\n        (budget: EvidenceBudget)\n        (riskProfile: DeterministicRiskProfile option)\n        (redactionPatterns: string list)\n        (diff: DiffDto)\n        =\n        let maxFiles = max 0 budget.MaxFiles\n        let maxHunks = max 0 budget.MaxHunksPerFile\n        let maxLines = max 0 budget.MaxLinesPerHunk\n        let maxTotalBytes = if budget.MaxTotalBytes <= 0 then Int32.MaxValue else budget.MaxTotalBytes\n\n        let slices = List<EvidenceSlice>()\n        let sliceSummaries = List<EvidenceSliceSummary>()\n        let mutable totalBytes = 0\n\n        let fileDiffs =\n            diff.FileDiffs\n            |> Seq.cast<FileDiff>\n            |> Seq.sortBy (fun fileDiff -> fileDiff.RelativePath)\n            |> Seq.truncate maxFiles\n            |> Seq.toList\n\n        for fileDiff in fileDiffs do\n            if not fileDiff.IsBinary then\n                let sections =\n                    fileDiff.InlineDiff\n                    |> Seq.cast<DiffPiece array>\n                    |> Seq.truncate maxHunks\n                    |> Seq.toList\n\n                for section in sections do\n                    let limitedLines = if maxLines = 0 then section else section |> Array.truncate maxLines\n\n                    let content =\n                        limitedLines\n                        |> Array.map (fun piece -> piece.Text)\n                        |> String.concat Environment.NewLine\n\n                    let redactedContent, isRedacted = redact redactionPatterns content\n                    let bytes = Encoding.UTF8.GetByteCount(redactedContent)\n\n                    if totalBytes + bytes <= maxTotalBytes then\n                        let startLine, endLine = getLineBounds limitedLines\n                        let score, reasons = scoreReasons riskProfile fileDiff.RelativePath limitedLines\n\n                        slices.Add(\n                            {\n                                RelativePath = fileDiff.RelativePath\n                                StartLine = startLine\n                                EndLine = endLine\n                                Content = redactedContent\n                                IsRedacted = isRedacted\n                            }\n                        )\n\n                        sliceSummaries.Add({ RelativePath = fileDiff.RelativePath; StartLine = startLine; EndLine = endLine; Score = score; Reasons = reasons })\n\n                        totalBytes <- totalBytes + bytes\n\n        let estimatedTokens = estimateTokens totalBytes\n\n        let evidenceSet = { Stage = stage; Slices = slices |> Seq.toList; Budget = budget; TotalBytes = totalBytes; EstimatedTokens = estimatedTokens }\n\n        let evidenceSummary =\n            {\n                Stage = stage\n                SelectedFiles =\n                    fileDiffs\n                    |> List.map (fun fileDiff -> fileDiff.RelativePath)\n                SliceSummaries = sliceSummaries |> Seq.toList\n                Budget = budget\n                TotalBytes = totalBytes\n                EstimatedTokens = estimatedTokens\n                TopReasons = topReasons (sliceSummaries |> Seq.toList)\n            }\n\n        evidenceSet, evidenceSummary\n"
  },
  {
    "path": "src/Grace.Shared/Extensions.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen System\nopen System.Collections.Generic\nopen System.Linq\nopen System.Runtime.CompilerServices\n\n[<assembly: InternalsVisibleTo(\"Host\")>]\ndo ()\n\nmodule Extensions =\n\n    type Dictionary<'T, 'U> with\n        /// Adds a range of key-value pairs to the dictionary.\n        member this.AddRange(items: seq<KeyValuePair<'T, 'U>>) =\n            items\n            |> Seq.iter (fun kvp -> this.Add(kvp.Key, kvp.Value))\n"
  },
  {
    "path": "src/Grace.Shared/Grace.Shared.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n    <PropertyGroup>\n        <TargetFramework>net10.0</TargetFramework>\n        <LangVersion>preview</LangVersion>\n        <PublishReadyToRun>true</PublishReadyToRun>\n        <Version>0.1</Version>\n        <Description>The shared core module for Grace.</Description>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n        <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n        <NoWarn>67;1057,3391</NoWarn>\n        <WarningsAsErrors>FS0025</WarningsAsErrors>\n        <Platforms>AnyCPU;x64</Platforms>\n        <OtherFlags>--test:GraphBasedChecking</OtherFlags>\n        <OtherFlags>--test:ParallelOptimization</OtherFlags>\n        <OtherFlags>--test:ParallelIlxGen</OtherFlags>\n    </PropertyGroup>\n    <ItemGroup>\n        <None Include=\"instructions.md\" />\n        <Compile Include=\"Extensions.Shared.fs\" />\n        <Compile Include=\"Constants.Shared.fs\" />\n        <Compile Include=\"AzureEnvironment.Shared.fs\" />\n        <Compile Include=\"Resources\\Text\\Languages.Resources.fs\" />\n        <Compile Include=\"Resources\\Text\\en-US.fs\" />\n        <Compile Include=\"Resources\\Utilities.Resources.fs\" />\n        <Compile Include=\"Combinators.fs\" />\n        <Compile Include=\"Utilities.Shared.fs\" />\n        <Compile Include=\"Authorization.Shared.fs\" />\n        <Compile Include=\"Converters\\BranchDtoConverter.Shared.fs\" />\n        <Compile Include=\"Services.Shared.fs\" />\n        <Compile Include=\"Diff.Shared.fs\" />\n        <Compile Include=\"Evidence.Shared.fs\" />\n        <Compile Include=\"ReviewNotes.Shared.fs\" />\n        <Compile Include=\"BaselineDrift.Shared.fs\" />\n        <Compile Include=\"Client\\Theme.Shared.fs\" />\n        <Compile Include=\"Client\\Configuration.Shared.fs\" />\n        <Compile Include=\"Client\\UserConfiguration.Shared.fs\" />\n        <Compile Include=\"Parameters\\Common.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Auth.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Access.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Owner.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Organization.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Repository.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Branch.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Reference.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Directory.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Diff.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Storage.Parameters.fs\" />\n        \n        <Compile Include=\"Parameters\\Reminder.Parameters.fs\" />\n        <Compile Include=\"Parameters\\WorkItem.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Policy.Parameters.fs\" />\n        <Compile Include=\"Parameters\\PromotionSet.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Review.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Queue.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Validation.Parameters.fs\" />\n        <Compile Include=\"Parameters\\Artifact.Parameters.fs\" />\n        <Compile Include=\"Validation\\Errors.Validation.fs\" />\n        <Compile Include=\"Validation\\Common.Validation.fs\" />\n        <Compile Include=\"Validation\\Utilities.Validation.fs\" />\n        <Compile Include=\"Validation\\Connect.Validation.fs\" />\n        <Compile Include=\"Validation\\Repository.Validation.fs\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <content Include=\"Monikers.imagemanifest\">\n            <IncludeInVSIX>true</IncludeInVSIX>\n        </content>\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n        <PackageReference Include=\"DiffPlex\" Version=\"1.9.0\" />\n        <PackageReference Include=\"FSharp.Control.TaskSeq\" Version=\"0.4.0\" />\n        <PackageReference Include=\"FSharp.SystemTextJson\" Version=\"1.4.36\" />\n        <PackageReference Include=\"FSharpPlus\" Version=\"1.8.0\" />\n        <PackageReference Include=\"MessagePack\" Version=\"3.1.4\" />\n        <PackageReference Include=\"MessagePack.Annotations\" Version=\"3.1.4\" />\n        <PackageReference Include=\"MessagePack.FSharpExtensions\" Version=\"4.0.0\" />\n        <PackageReference Include=\"MessagePack.NodaTime\" Version=\"3.5.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.ObjectPool\" Version=\"10.0.0\" />\n        <PackageReference Include=\"Microsoft.Orleans.Core\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Sdk\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Serialization.FSharp\" Version=\"9.2.1\" />\n        <PackageReference Include=\"Microsoft.Orleans.Serialization.SystemTextJson\" Version=\"9.2.1\" />\n        <PackageReference Include=\"MimeTypeMapOfficial\" Version=\"1.0.17\" />\n        <PackageReference Include=\"MimeTypes\" Version=\"2.5.2\" />\n        <PackageReference Include=\"Nanoid\" Version=\"3.1.0\" />\n        <PackageReference Include=\"NodaTime\" Version=\"3.2.2\" />\n        <PackageReference Include=\"NodaTime.Serialization.SystemTextJson\" Version=\"1.3.0\" />\n        <PackageReference Include=\"Polly\" Version=\"8.6.5\" />\n        <PackageReference Include=\"Polly.Contrib.WaitAndRetry\" Version=\"1.1.1\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Shared/Monikers.imagemanifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file was generated by the ManifestFromResources tool.-->\n<!-- Version: 14.0.50929.2 -->\n<ImageManifest xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns=\"http://schemas.microsoft.com/VisualStudio/ImageManifestSchema/2014\">\n  <Symbols>\n    <String Name=\"Resources\" Value=\"/Grace.Shared;Component/../Grace.Shared\" />\n    <Guid Name=\"MonikersGuid\" Value=\"{39d4a046-0220-4a60-90cc-bc0eebcd9f6d}\" />\n  </Symbols>\n  <Images />\n  <ImageLists />\n</ImageManifest>"
  },
  {
    "path": "src/Grace.Shared/Parameters/Access.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\nopen System.Collections.Generic\n\nmodule Access =\n\n    type AccessParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public BranchId = String.Empty with get, set\n\n    type GrantRoleParameters() =\n        inherit AccessParameters()\n        member val public PrincipalType = String.Empty with get, set\n        member val public PrincipalId = String.Empty with get, set\n        member val public ScopeKind = String.Empty with get, set\n        member val public RoleId = String.Empty with get, set\n        member val public Source = String.Empty with get, set\n        member val public SourceDetail = String.Empty with get, set\n\n    type RevokeRoleParameters() =\n        inherit AccessParameters()\n        member val public PrincipalType = String.Empty with get, set\n        member val public PrincipalId = String.Empty with get, set\n        member val public ScopeKind = String.Empty with get, set\n        member val public RoleId = String.Empty with get, set\n\n    type ListRoleAssignmentsParameters() =\n        inherit AccessParameters()\n        member val public PrincipalType = String.Empty with get, set\n        member val public PrincipalId = String.Empty with get, set\n        member val public ScopeKind = String.Empty with get, set\n\n    type ClaimPermissionParameters() =\n        member val public Claim = String.Empty with get, set\n        member val public DirectoryPermission = String.Empty with get, set\n\n    type UpsertPathPermissionParameters() =\n        inherit AccessParameters()\n        member val public Path = String.Empty with get, set\n        member val public ClaimPermissions = List<ClaimPermissionParameters>() with get, set\n\n    type RemovePathPermissionParameters() =\n        inherit AccessParameters()\n        member val public Path = String.Empty with get, set\n\n    type ListPathPermissionsParameters() =\n        inherit AccessParameters()\n        member val public Path = String.Empty with get, set\n\n    type CheckPermissionParameters() =\n        inherit AccessParameters()\n        member val public Operation = String.Empty with get, set\n        member val public ResourceKind = String.Empty with get, set\n        member val public Path = String.Empty with get, set\n        member val public PrincipalType = String.Empty with get, set\n        member val public PrincipalId = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Artifact.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\n\nmodule Artifact =\n\n    /// Base parameters for artifact endpoints.\n    type ArtifactParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n\n    /// Parameters for POST /artifact/create.\n    type CreateArtifactParameters() =\n        inherit ArtifactParameters()\n        member val public ArtifactId = String.Empty with get, set\n        member val public ArtifactType = String.Empty with get, set\n        member val public MimeType = String.Empty with get, set\n        member val public Size = 0L with get, set\n        member val public Sha256 = String.Empty with get, set\n\n    /// Parameters for GET /artifact/{artifactId}/download-uri.\n    type GetArtifactDownloadUriParameters() =\n        inherit ArtifactParameters()\n        member val public ArtifactId = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Auth.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.PersonalAccessToken\nopen Orleans\nopen System\n\nmodule Auth =\n    [<GenerateSerializer>]\n    type AuthParameters() =\n        inherit CommonParameters()\n\n    type CreatePersonalAccessTokenParameters() =\n        inherit AuthParameters()\n        member val public TokenName = String.Empty with get, set\n        member val public ExpiresInSeconds = 0L with get, set\n        member val public NoExpiry = false with get, set\n\n    type ListPersonalAccessTokensParameters() =\n        inherit AuthParameters()\n        member val public IncludeRevoked = false with get, set\n        member val public IncludeExpired = false with get, set\n\n    type RevokePersonalAccessTokenParameters() =\n        inherit AuthParameters()\n        member val public TokenId: PersonalAccessTokenId = Guid.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Branch.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\n\nmodule Branch =\n\n    /// Parameters for many endpoints in the /branch path.\n    type BranchParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName: OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName: OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName: RepositoryName = String.Empty with get, set\n        member val public BranchId = String.Empty with get, set\n        member val public BranchName: BranchName = String.Empty with get, set\n\n    /// Base class for parameters for branch queries.\n    type BranchQueryParameters() =\n        inherit BranchParameters()\n        member val public Sha256Hash: Sha256Hash = String.Empty with get, set\n        member val public ReferenceId = String.Empty with get, set\n\n    /// Parameters for the /branch/create endpoint.\n    type CreateBranchParameters() =\n        inherit BranchParameters()\n        member val public ParentBranchId = String.Empty with get, set\n        member val public ParentBranchName: BranchName = String.Empty with get, set\n        member val public InitialPermissions: ReferenceType seq = [ Commit; Checkpoint; Save; Tag ] with get, set\n\n    /// Parameters for the /branch/assign endpoint.\n    type AssignParameters() =\n        inherit BranchParameters()\n        member val public DirectoryVersionId: DirectoryVersionId = Guid.Empty with get, set\n        member val public Sha256Hash: Sha256Hash = String.Empty with get, set\n        member val public Message = String.Empty with get, set\n\n    /// Parameters for the /branch/rebase endpoint.\n    type RebaseParameters() =\n        inherit BranchParameters()\n        member val public BasedOn: ReferenceId = ReferenceId.Empty with get, set\n\n    /// Parameters for the various /branch/create[reference] endpoints.\n    type CreateReferenceParameters() =\n        inherit BranchParameters()\n        member val public DirectoryVersionId: DirectoryVersionId = Guid.Empty with get, set\n        member val public Sha256Hash: Sha256Hash = String.Empty with get, set\n        member val public Message = String.Empty with get, set\n\n    /// Parameters for the /branch/setName endpoint.\n    type SetBranchNameParameters() =\n        inherit BranchParameters()\n        member val public NewName = String.Empty with get, set\n\n    /// Parameters for the various /branch/enable[feature] endpoints.\n    type EnableFeatureParameters() =\n        inherit BranchParameters()\n        member val public Enabled = false with get, set\n\n    /// Parameters for the /branch/setPromotionMode endpoint.\n    type SetPromotionModeParameters() =\n        inherit BranchParameters()\n        member val public PromotionMode = String.Empty with get, set\n\n    /// Parameters for the /branch/delete endpoint.\n    type DeleteBranchParameters() =\n        inherit BranchParameters()\n        member val public Force: bool = false with get, set\n        member val public DeleteReason: DeleteReason = String.Empty with get, set\n        member val public ReassignChildBranches: bool = false with get, set\n        member val public NewParentBranchId: string = String.Empty with get, set\n        member val public NewParentBranchName: string = String.Empty with get, set\n\n    /// Parameters for the /branch/updateParentBranch endpoint.\n    type UpdateParentBranchParameters() =\n        inherit BranchParameters()\n        member val public NewParentBranchId: string = String.Empty with get, set\n        member val public NewParentBranchName: string = String.Empty with get, set\n\n    /// Parameters for the /branch/getReference endpoint.\n    type GetReferenceParameters() =\n        inherit BranchQueryParameters()\n\n    /// Parameters for the /branch/getReferences and /branch/get[reference] endpoints.\n    type GetReferencesParameters() =\n        inherit BranchParameters()\n        member val public FullSha = false with get, set\n        member val public MaxCount = 50 with get, set\n\n    type GetLatestReferencesByReferenceTypeParameters() =\n        inherit BranchParameters()\n\n        member val public ReferenceTypes: ReferenceType array =\n            [|\n                Promotion\n                Commit\n                Checkpoint\n                Save\n            |] with get, set\n\n    /// Parameters for the /branch/getDiffsForReferenceType endpoint.\n    type GetDiffsForReferenceTypeParameters() =\n        inherit BranchParameters()\n        member val public ReferenceType = String.Empty with get, set\n        member val public MaxCount = 50 with get, set\n\n    /// Parameters for the /branch/getDiffsForReferences endpoint.\n    type GetDiffsForReferencesParameters() =\n        inherit BranchParameters()\n        member val public References = String.Empty with get, set\n        member val public MaxCount = 50 with get, set\n\n    /// Parameters for the /branch/get endpoint.\n    type GetBranchParameters() =\n        inherit BranchQueryParameters()\n        member val public IncludeDeleted = false with get, set\n\n    type ListContentsParameters() =\n        inherit BranchQueryParameters()\n        member val public Sha256Hash: Sha256Hash = String.Empty with get, set\n        member val public ReferenceId = String.Empty with get, set\n        member val public Pattern = String.Empty with get, set\n        member val public ShowDirectories = true with get, set\n        member val public ShowFiles = true with get, set\n        member val public ForceRecompute = false with get, set\n\n    /// Parameters for the /branch/switch endpoint.\n    type SwitchParameters() =\n        inherit BranchQueryParameters()\n\n    /// Parameters for the /branch/getVersion endpoint.\n    type GetBranchVersionParameters() =\n        inherit BranchQueryParameters()\n        member val public IncludeDeleted = false with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Common.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Orleans\nopen System\n\nmodule Common =\n\n    [<GenerateSerializer>]\n    type CommonParameters() =\n        member val public CorrelationId: string = String.Empty with get, set\n        member val public Principal: string = String.Empty with get, set\n\n    /// Base parameters for /agent/session endpoints.\n    type AgentSessionParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n        member val public AgentId = String.Empty with get, set\n        member val public AgentDisplayName = String.Empty with get, set\n\n    /// Parameters for /agent/session/start.\n    ///\n    /// OperationId is a caller-supplied idempotency token for deterministic replay handling.\n    type StartAgentSessionParameters() =\n        inherit AgentSessionParameters()\n        member val public WorkItemIdOrNumber = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n        member val public Source = String.Empty with get, set\n        member val public OperationId = String.Empty with get, set\n\n    /// Parameters for /agent/session/stop.\n    ///\n    /// OperationId is a caller-supplied idempotency token for deterministic replay handling.\n    type StopAgentSessionParameters() =\n        inherit AgentSessionParameters()\n        member val public SessionId = String.Empty with get, set\n        member val public WorkItemIdOrNumber = String.Empty with get, set\n        member val public StopReason = String.Empty with get, set\n        member val public OperationId = String.Empty with get, set\n\n    /// Parameters for /agent/session/status.\n    type GetAgentSessionStatusParameters() =\n        inherit AgentSessionParameters()\n        member val public SessionId = String.Empty with get, set\n        member val public WorkItemIdOrNumber = String.Empty with get, set\n\n    /// Parameters for /agent/session/active.\n    type GetActiveAgentSessionParameters() =\n        inherit AgentSessionParameters()\n        member val public WorkItemIdOrNumber = String.Empty with get, set\n\n    /// Parameters for /agent/session/listActive.\n    type ListActiveAgentSessionsParameters() =\n        inherit AgentSessionParameters()\n        member val public MaximumSessionCount = 25 with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Diff.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\n\nmodule Diff =\n\n    /// Parameters used by multiple endpoints in the /diff path.\n    type DiffParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName: OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName: OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName: RepositoryName = String.Empty with get, set\n        member val public DirectoryVersionId1: DirectoryVersionId = DirectoryVersionId.Empty with get, set\n        member val public DirectoryVersionId2: DirectoryVersionId = DirectoryVersionId.Empty with get, set\n\n    /// Parameters used by the /diff/populate endpoint.\n    type PopulateParameters() =\n        inherit DiffParameters()\n\n    /// Parameters used by the /diff/get endpoint.\n    type GetDiffParameters() =\n        inherit DiffParameters()\n\n    type GetDiffByReferenceTypeParameters() =\n        inherit DiffParameters()\n        member val public BranchId = String.Empty with get, set\n        member val public BranchName = BranchName String.Empty with get, set\n\n    /// Parameters used by the /diff/getDiffBySha256Hash endpoint.\n    type GetDiffBySha256HashParameters() =\n        inherit DiffParameters()\n        member val public Sha256Hash1 = Sha256Hash String.Empty with get, set\n        member val public Sha256Hash2 = Sha256Hash String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Directory.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\nopen System.Collections.Generic\n\nmodule DirectoryVersion =\n\n    type DirectoryVersionParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName: OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName: OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName: RepositoryName = String.Empty with get, set\n        member val public DirectoryVersionId = String.Empty with get, set\n\n    type CreateParameters() =\n        inherit DirectoryVersionParameters()\n        member val public DirectoryVersion = DirectoryVersion.Default with get, set\n\n    type GetParameters() =\n        inherit DirectoryVersionParameters()\n\n    type GetByDirectoryIdsParameters() =\n        inherit DirectoryVersionParameters()\n        member val public DirectoryIds = List<DirectoryVersionId>() with get, set\n\n    type GetBySha256HashParameters() =\n        inherit DirectoryVersionParameters()\n        member val public Sha256Hash = String.Empty with get, set\n\n    type GetZipFileParameters() =\n        inherit DirectoryVersionParameters()\n        member val public Sha256Hash = String.Empty with get, set\n\n    type SaveDirectoryVersionsParameters() =\n        inherit DirectoryVersionParameters()\n        member val public DirectoryVersions = List<DirectoryVersion>() with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Organization.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\n\nmodule Organization =\n\n    /// Common parameters for endpoints in the /organization path.\n    type OrganizationParameters() =\n        inherit CommonParameters()\n        /// The Id of the Owner.\n        member val public OwnerId: string = String.Empty with get, set\n        /// The name of the Owner.\n        member val public OwnerName: string = String.Empty with get, set\n        /// The Id of the Organization.\n        member val public OrganizationId: string = String.Empty with get, set\n        /// The name of the Organization.\n        member val public OrganizationName: string = String.Empty with get, set\n\n    /// Parameters for the /organization/create endpoint.\n    type CreateOrganizationParameters() =\n        inherit OrganizationParameters()\n\n    /// Parameters for the /organization/get endpoint.\n    type GetOrganization() =\n        inherit OrganizationParameters()\n        /// If true, deleted Organizations will be included in the response.\n        member val public IncludeDeleted: bool = false with get, set\n\n    /// Parameters for the /organization/setName endpoint.\n    type SetOrganizationNameParameters() =\n        inherit OrganizationParameters()\n        /// The new name of the Organization.\n        member val public NewName: string = String.Empty with get, set\n\n    /// Parameters for the /organization/setType endpoint.\n    type SetOrganizationTypeParameters() =\n        inherit OrganizationParameters()\n        /// The new type of the Organization. Must be one of the OrganizationType enum values.\n        member val public OrganizationType: string = String.Empty with get, set\n\n    /// Parameters for the /organization/setSearchVisibility endpoint.\n    type SetOrganizationSearchVisibilityParameters() =\n        inherit OrganizationParameters()\n        /// The new search visibility of the Organization. Must be one of the SearchVisibility enum values.\n        member val public SearchVisibility: string = String.Empty with get, set\n\n    /// Parameters for the /organization/setDescription endpoint.\n    type SetOrganizationDescriptionParameters() =\n        inherit OrganizationParameters()\n        /// The new description of the Organization.\n        member val public Description: string = String.Empty with get, set\n\n    /// Parameters for the /organization/delete endpoint.\n    type DeleteOrganizationParameters() =\n        inherit OrganizationParameters()\n        /// If true, the Organization will be deleted even if it has Repositories.\n        member val public Force: bool = false with get, set\n        /// The reason for deleting the Organization.\n        member val public DeleteReason: string = String.Empty with get, set\n\n    /// Parameters for the /organization/undelete endpoint.\n    type UndeleteOrganizationParameters() =\n        inherit OrganizationParameters()\n\n    /// Parameters for the /organization/get endpoint.\n    type GetOrganizationParameters() =\n        inherit OrganizationParameters()\n        member val public IncludeDeleted = false with get, set\n\n    /// Parameters for the /organization/listRepositories endpoint.\n    type ListRepositoriesParameters() =\n        inherit OrganizationParameters()\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Owner.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\nopen Orleans\n\nmodule Owner =\n\n    /// Common parameters for endpoints in the /owner path.\n    [<GenerateSerializer>]\n    type OwnerParameters() =\n        inherit CommonParameters()\n        /// The Id of the owner.\n        member val public OwnerId: string = String.Empty with get, set\n        /// The name of the owner.\n        member val public OwnerName: string = String.Empty with get, set\n\n    /// Parameters for the /owner/create endpoint.\n    [<GenerateSerializer>]\n    type CreateOwnerParameters() =\n        inherit OwnerParameters()\n\n    /// Parameters for the /owner/setName endpoint.\n    type SetOwnerNameParameters() =\n        inherit OwnerParameters()\n        /// The new name for the owner.\n        member val public NewName: string = String.Empty with get, set\n\n    /// Parameters for the /owner/setType endpoint.\n    type SetOwnerTypeParameters() =\n        inherit OwnerParameters()\n        /// The new type for the owner. Must be one of the OwnerType cases.\n        member val public OwnerType: string = String.Empty with get, set\n\n    /// Parameters for the /owner/setSearchVisibility endpoint.\n    type SetOwnerSearchVisibilityParameters() =\n        inherit OwnerParameters()\n        /// The new search visibility for the owner. Must be one of the SearchVisibility cases.\n        member val public SearchVisibility: string = String.Empty with get, set\n\n    /// Parameters for the /owner/setDescription endpoint.\n    type SetOwnerDescriptionParameters() =\n        inherit OwnerParameters()\n        /// The new description for the owner.\n        member val public Description: string = String.Empty with get, set\n\n    /// Parameters for the /owner/get endpoint.\n    type GetOwnerParameters() =\n        inherit OwnerParameters()\n        /// If true, include deleted owners in the response.\n        member val public IncludeDeleted: bool = false with get, set\n\n    /// Parameters for the /owner/delete endpoint.\n    type DeleteOwnerParameters() =\n        inherit OwnerParameters()\n        /// If true, force the deletion of the owner even if it has repositories.\n        member val public Force: bool = false with get, set\n        /// The reason for the deletion.\n        member val public DeleteReason: DeleteReason = String.Empty with get, set\n\n    /// Parameters for the /owner/undelete endpoint.\n    type UndeleteOwnerParameters() =\n        inherit OwnerParameters()\n\n    /// Parameters for the /owner/listOrganizations endpoint.\n    type ListOrganizationsParameters() =\n        inherit OwnerParameters()\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Policy.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\n\nmodule Policy =\n    /// Base parameters for policy endpoints.\n    type PolicyParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n        member val public TargetBranchId = String.Empty with get, set\n\n    /// Parameters for /policy/current.\n    type GetPolicyParameters() =\n        inherit PolicyParameters()\n\n    /// Parameters for /policy/acknowledge.\n    type AcknowledgePolicyParameters() =\n        inherit PolicyParameters()\n        member val public PolicySnapshotId = String.Empty with get, set\n        member val public Note = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/PromotionSet.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.PromotionSet\nopen System\n\nmodule PromotionSet =\n    /// Base parameters for promotion-set endpoints.\n    type PromotionSetParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n\n    /// Parameters for /promotion-set/get.\n    type GetPromotionSetParameters() =\n        inherit PromotionSetParameters()\n\n    /// Parameters for /promotion-set/get-events.\n    type GetPromotionSetEventsParameters() =\n        inherit PromotionSetParameters()\n\n    /// Parameters for /promotion-set/create.\n    type CreatePromotionSetParameters() =\n        inherit PromotionSetParameters()\n        member val public TargetBranchId = String.Empty with get, set\n\n    /// Parameters for /promotion-set/update-input-promotions.\n    type UpdatePromotionSetInputPromotionsParameters() =\n        inherit PromotionSetParameters()\n        member val public PromotionPointers: PromotionPointer list = [] with get, set\n\n    /// Parameters for /promotion-set/recompute.\n    type RecomputePromotionSetParameters() =\n        inherit PromotionSetParameters()\n        member val public Reason = String.Empty with get, set\n\n    /// Parameters for /promotion-set/apply.\n    type ApplyPromotionSetParameters() =\n        inherit PromotionSetParameters()\n\n    /// Parameters for /promotion-set/{promotionSetId}/resolve-conflicts.\n    type ResolvePromotionSetConflictsParameters() =\n        inherit PromotionSetParameters()\n        member val public StepId = String.Empty with get, set\n        member val public Decisions: ConflictResolutionDecision list = [] with get, set\n        member val public StepsComputationAttempt = 0 with get, set\n\n    /// Parameters for /promotion-set/delete.\n    type DeletePromotionSetParameters() =\n        inherit PromotionSetParameters()\n        member val public Force = false with get, set\n        member val public DeleteReason = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Queue.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\n\nmodule Queue =\n    /// Base parameters for queue endpoints.\n    type QueueParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n        member val public TargetBranchId = String.Empty with get, set\n        member val public PolicySnapshotId = String.Empty with get, set\n        member val public WorkItemId = String.Empty with get, set\n\n    /// Parameters for /queue/status.\n    type QueueStatusParameters() =\n        inherit QueueParameters()\n\n    /// Parameters for /queue/pause and /queue/resume.\n    type QueueActionParameters() =\n        inherit QueueParameters()\n\n    /// Parameters for /queue/enqueue.\n    type EnqueueParameters() =\n        inherit QueueParameters()\n        member val public PromotionSetId = String.Empty with get, set\n\n    /// Parameters for /queue/dequeue.\n    type PromotionSetActionParameters() =\n        inherit QueueParameters()\n        member val public PromotionSetId = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Reference.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\n\nmodule Reference =\n\n    /// Parameters for many endpoints in the /branch path.\n    type ReferenceParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName: OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName: OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName: RepositoryName = String.Empty with get, set\n        member val public BranchId = String.Empty with get, set\n        member val public BranchName: BranchName = String.Empty with get, set\n\n    /// Parameters for the /reference/assign endpoint.\n    type AssignParameters() =\n        inherit ReferenceParameters()\n        member val public DirectoryVersionId: DirectoryVersionId = Guid.Empty with get, set\n        member val public Sha256Hash: Sha256Hash = String.Empty with get, set\n        member val public Message = String.Empty with get, set\n\n    /// Parameters for the various /reference/create[reference] endpoints.\n    type CreateReferenceParameters() =\n        inherit ReferenceParameters()\n        member val public DirectoryVersionId: DirectoryVersionId = Guid.Empty with get, set\n        member val public Sha256Hash: Sha256Hash = String.Empty with get, set\n        member val public Message = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Reminder.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Shared\nopen Grace.Types.Types\nopen NodaTime\nopen System\nopen System.Collections.Generic\n\nmodule Reminder =\n\n    /// Base parameters for reminder admin endpoints - scoped by repository.\n    type ReminderParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n\n    /// Parameters for listing reminders.\n    type ListRemindersParameters() =\n        inherit ReminderParameters()\n        /// Maximum number of reminders to return.\n        member val public MaxCount: int = Constants.DefaultReminderMaxCount with get, set\n        /// Filter by reminder type (e.g., Maintenance, PhysicalDeletion).\n        member val public ReminderType = String.Empty with get, set\n        /// Filter by actor name (e.g., Branch, Repository).\n        member val public ActorName = String.Empty with get, set\n        /// Filter by status (pending, dispatched, failed).\n        member val public Status = String.Empty with get, set\n        /// Filter by reminders due after this time (ISO8601).\n        member val public DueAfter = String.Empty with get, set\n        /// Filter by reminders due before this time (ISO8601).\n        member val public DueBefore = String.Empty with get, set\n\n    /// Parameters for getting a single reminder.\n    type GetReminderParameters() =\n        inherit ReminderParameters()\n        /// The unique ID of the reminder to get.\n        member val public ReminderId = String.Empty with get, set\n\n    /// Parameters for deleting a reminder.\n    type DeleteReminderParameters() =\n        inherit ReminderParameters()\n        /// The unique ID of the reminder to delete.\n        member val public ReminderId = String.Empty with get, set\n\n    /// Parameters for updating a reminder's fire time.\n    type UpdateReminderTimeParameters() =\n        inherit ReminderParameters()\n        /// The unique ID of the reminder to update.\n        member val public ReminderId = String.Empty with get, set\n        /// The new fire time in ISO8601 format.\n        member val public FireAt = String.Empty with get, set\n\n    /// Parameters for rescheduling a reminder relative to now or original time.\n    type RescheduleReminderParameters() =\n        inherit ReminderParameters()\n        /// The unique ID of the reminder to reschedule.\n        member val public ReminderId = String.Empty with get, set\n        /// The duration to add (e.g., +15m, +1h, +1d). Relative to now.\n        member val public After = String.Empty with get, set\n\n    /// Parameters for creating a manual reminder for testing/tooling.\n    type CreateReminderParameters() =\n        inherit ReminderParameters()\n        /// The target actor name (e.g., Branch, Repository, Owner).\n        member val public ActorName = String.Empty with get, set\n        /// The target actor ID.\n        member val public ActorId = String.Empty with get, set\n        /// The type of reminder (Maintenance, PhysicalDeletion, DeleteCachedState, DeleteZipFile).\n        member val public ReminderType = String.Empty with get, set\n        /// When the reminder should fire (ISO8601 format).\n        member val public FireAt = String.Empty with get, set\n        /// Optional JSON payload for the reminder state.\n        member val public StateJson = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Repository.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\nopen System.Collections.Generic\n\nmodule Repository =\n\n    /// Parameters for many endpoints in the /repository path.\n    type RepositoryParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n\n    /// Parameters for the /repository/create endpoint.\n    type CreateRepositoryParameters() =\n        inherit RepositoryParameters()\n        member val public ObjectStorageProvider = ObjectStorageProvider.DefaultObjectStorageProvider with get, set\n\n    /// Parameters for the /repository/get endpoint.\n    type GetRepositoryParameters() =\n        inherit RepositoryParameters()\n\n    /// Parameters for the /repository/isEmpty endpoint.\n    type IsEmptyParameters() =\n        inherit RepositoryParameters()\n\n    /// Parameters for the /repository/init endpoint.\n    type InitParameters() =\n        inherit RepositoryParameters()\n        member val public GraceConfig = String.Empty with get, set\n\n    /// Parameters for the /repository/setVisibility endpoint.\n    type SetRepositoryVisibilityParameters() =\n        inherit RepositoryParameters()\n        member val public Visibility = String.Empty with get, set\n\n    /// Parameters for the /repository/setStatus endpoint.\n    type SetRepositoryStatusParameters() =\n        inherit RepositoryParameters()\n        member val public Status = String.Empty with get, set\n\n    /// Parameters for the /repository/setAnonymousAccess endpoint.\n    type SetAnonymousAccessParameters() =\n        inherit RepositoryParameters()\n        member val public AnonymousAccess = false with get, set\n\n    /// Parameters for the /repository/setAllowsLargeFiles endpoint.\n    type SetAllowsLargeFilesParameters() =\n        inherit RepositoryParameters()\n        member val public AllowsLargeFiles = false with get, set\n\n    /// Parameters for the /repository/recordSaves endpoint.\n    type RecordSavesParameters() =\n        inherit RepositoryParameters()\n        member val public RecordSaves = false with get, set\n\n    /// Parameters for the /repository/setLogicalDeleteDays endpoint.\n    type SetLogicalDeleteDaysParameters() =\n        inherit RepositoryParameters()\n        member val public LogicalDeleteDays: single = Single.MinValue with get, set\n\n    /// Parameters for the /repository/setSaveDays endpoint.\n    type SetSaveDaysParameters() =\n        inherit RepositoryParameters()\n        member val public SaveDays: single = Single.MinValue with get, set\n\n    /// Parameters for the /repository/setCheckpointDays endpoint.\n    type SetCheckpointDaysParameters() =\n        inherit RepositoryParameters()\n        member val public CheckpointDays: single = Single.MinValue with get, set\n\n    /// Parameters for the /repository/setDirectoryVersionCacheDays endpoint.\n    type SetDirectoryVersionCacheDaysParameters() =\n        inherit RepositoryParameters()\n        member val public DirectoryVersionCacheDays: single = Single.MinValue with get, set\n\n    /// Parameters for the /repository/setDiffCacheDays endpoint.\n    type SetDiffCacheDaysParameters() =\n        inherit RepositoryParameters()\n        member val public DiffCacheDays: single = Single.MinValue with get, set\n\n    /// Parameters for the /repository/setDescription endpoint.\n    type SetRepositoryDescriptionParameters() =\n        inherit RepositoryParameters()\n        member val public Description = String.Empty with get, set\n\n    /// Parameters for the /repository/setDefaultServerApiVersion endpoint.\n    type SetDefaultServerApiVersionParameters() =\n        inherit RepositoryParameters()\n        member val public DefaultServerApiVersion = String.Empty with get, set\n\n    /// Parameters for the /repository/setName endpoint.\n    type SetRepositoryNameParameters() =\n        inherit RepositoryParameters()\n        member val public NewName = String.Empty with get, set\n\n    /// Parameters for the /repository/delete endpoint.\n    type DeleteRepositoryParameters() =\n        inherit RepositoryParameters()\n        member val public Force = false with get, set\n        member val public DeleteReason = String.Empty with get, set\n\n    type EnablePromotionTypeParameters() =\n        inherit RepositoryParameters()\n        member val public Enabled = false with get, set\n\n    /// Parameters for the /repository/getReferencesByReferenceId endpoint.\n    type GetReferencesByReferenceIdParameters() =\n        inherit RepositoryParameters()\n        member val public ReferenceIds: IEnumerable<ReferenceId> = Array.Empty<ReferenceId>() with get, set\n        member val public MaxCount: int = 1 with get, set\n\n    /// Parameters for the /repository/getBranched endpoint.\n    type GetBranchesParameters() =\n        inherit RepositoryParameters()\n        member val public IncludeDeleted = false with get, set\n        member val public MaxCount: int = 30 with get, set\n\n    /// Parameters for the /repository/getBranchesByBranchId endpoint.\n    type GetBranchesByBranchIdParameters() =\n        inherit RepositoryParameters()\n        member val public BranchIds: IEnumerable<BranchId> = Array.Empty<BranchId>() with get, set\n        member val public MaxCount: int = 1 with get, set\n        member val public IncludeDeleted = false with get, set\n\n    /// Parameters for the /repository/undelete endpoint.\n    type UndeleteRepositoryParameters() =\n        inherit RepositoryParameters()\n\n    /// Parameters for the /repository/setConflictResolutionPolicy endpoint.\n    type SetConflictResolutionPolicyParameters() =\n        inherit RepositoryParameters()\n        member val public ConflictResolutionPolicy = String.Empty with get, set\n        member val public ConfidenceThreshold: float32 = 0.8f with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Review.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\n\nmodule Review =\n    [<RequireQualifiedAccess>]\n    module CandidateIdentityModes =\n        [<Literal>]\n        let DirectPromotionSetProjection = \"DirectPromotionSetProjection\"\n\n    [<RequireQualifiedAccess>]\n    module ProjectionSourceStates =\n        [<Literal>]\n        let Authoritative = \"Authoritative\"\n\n        [<Literal>]\n        let Inferred = \"Inferred\"\n\n        [<Literal>]\n        let NotAvailable = \"NotAvailable\"\n\n    /// Base parameters for review endpoints.\n    type ReviewParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n\n    /// Parameters for /review/notes.\n    type GetReviewNotesParameters() =\n        inherit ReviewParameters()\n\n    /// Parameters for /review/checkpoint.\n    type ReviewCheckpointParameters() =\n        inherit ReviewParameters()\n        member val public ReviewedUpToReferenceId = String.Empty with get, set\n        member val public PolicySnapshotId = String.Empty with get, set\n\n    /// Parameters for /review/resolve.\n    type ResolveFindingParameters() =\n        inherit ReviewParameters()\n        member val public FindingId = String.Empty with get, set\n        member val public ResolutionState = String.Empty with get, set\n        member val public Note = String.Empty with get, set\n\n    /// Parameters for /review/deepen.\n    type DeepenReviewParameters() =\n        inherit ReviewParameters()\n        member val public ChapterId = String.Empty with get, set\n\n    /// Base parameters for candidate projection endpoints.\n    type CandidateProjectionParameters() =\n        inherit ReviewParameters()\n        member val public CandidateId = String.Empty with get, set\n\n    /// Parameters for candidate identity projection.\n    type ResolveCandidateIdentityParameters() =\n        inherit CandidateProjectionParameters()\n\n    /// Parameters for rerunning a candidate gate.\n    type CandidateGateRerunParameters() =\n        inherit CandidateProjectionParameters()\n        member val public Gate = String.Empty with get, set\n\n    /// Repository scope attached to candidate projection identity.\n    type CandidateProjectionScope() =\n        member val public OwnerId = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n\n    /// Candidate-to-promotion-set identity projection contract.\n    type CandidateIdentityProjection() =\n        member val public CandidateId = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n\n        member val public IdentityMode = CandidateIdentityModes.DirectPromotionSetProjection with get, set\n\n        member val public Scope = CandidateProjectionScope() with get, set\n\n    /// Source state metadata for projected sections.\n    type ProjectionSourceStateMetadata() =\n        member val public Section = String.Empty with get, set\n        member val public SourceState = ProjectionSourceStates.NotAvailable with get, set\n        member val public Detail = String.Empty with get, set\n\n    /// Candidate identity projection result with section source-state metadata.\n    type CandidateIdentityProjectionResult() =\n        member val public Identity = CandidateIdentityProjection() with get, set\n        member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n\n    /// Candidate snapshot projection result for `candidate get`.\n    type CandidateProjectionSnapshotResult() =\n        member val public Identity = CandidateIdentityProjection() with get, set\n        member val public PromotionSetStatus = String.Empty with get, set\n        member val public StepsComputationStatus = String.Empty with get, set\n        member val public QueueState = String.Empty with get, set\n        member val public RunningPromotionSetId = String.Empty with get, set\n        member val public UnresolvedFindingCount = 0 with get, set\n        member val public ValidationSummaryAvailable = false with get, set\n        member val public RequiredActions: string list = [] with get, set\n        member val public Diagnostics: string list = [] with get, set\n        member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n\n    /// Candidate required-actions projection result.\n    type CandidateRequiredActionsResult() =\n        member val public Identity = CandidateIdentityProjection() with get, set\n        member val public RequiredActions: string list = [] with get, set\n        member val public Diagnostics: string list = [] with get, set\n        member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n\n    /// Candidate attestation entry.\n    type CandidateAttestation() =\n        member val public Name = String.Empty with get, set\n        member val public Status = ProjectionSourceStates.NotAvailable with get, set\n        member val public Detail = String.Empty with get, set\n\n    /// Candidate attestation projection result.\n    type CandidateAttestationsResult() =\n        member val public Identity = CandidateIdentityProjection() with get, set\n        member val public Attestations: CandidateAttestation list = [] with get, set\n        member val public Diagnostics: string list = [] with get, set\n        member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n\n    /// Candidate action result for retry, cancel, and gate rerun operations.\n    type CandidateActionResult() =\n        member val public Identity = CandidateIdentityProjection() with get, set\n        member val public Action = String.Empty with get, set\n        member val public AppliedOperations: string list = [] with get, set\n        member val public Diagnostics: string list = [] with get, set\n        member val public SourceStates: ProjectionSourceStateMetadata list = [] with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Storage.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Types\nopen System\n\nmodule Storage =\n\n    /// Parameters used by multiple endpoints in the /diff path.\n    type StorageParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName: OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName: OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName: RepositoryName = String.Empty with get, set\n\n    /// Parameters used by the /diff/populate endpoint.\n    type DeleteAllParameters() =\n        inherit StorageParameters()\n\n    /// Parameters used by the /diff/get endpoint.\n    type GetUploadUriParameters() =\n        inherit StorageParameters()\n        member val public FileVersions = Array.empty<FileVersion> with get, set\n\n    type GetDownloadUriParameters() =\n        inherit StorageParameters()\n        member val public FileVersion = FileVersion.Default with get, set\n\n    type GetUploadMetadataForFilesParameters() =\n        inherit StorageParameters()\n        member val public FileVersions = Array.empty<FileVersion> with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/Validation.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Validation\nopen System\n\nmodule Validation =\n\n    /// Base parameters for validation-set and validation-result endpoints.\n    type ValidationParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n\n    /// Parameters for /validation-set/create.\n    type CreateValidationSetParameters() =\n        inherit ValidationParameters()\n        member val public ValidationSetId = String.Empty with get, set\n        member val public TargetBranchId = String.Empty with get, set\n        member val public Rules: ValidationSetRule list = [] with get, set\n        member val public Validations: Validation list = [] with get, set\n\n    /// Parameters for /validation-set/get.\n    type GetValidationSetParameters() =\n        inherit ValidationParameters()\n        member val public ValidationSetId = String.Empty with get, set\n\n    /// Parameters for /validation-set/update.\n    type UpdateValidationSetParameters() =\n        inherit ValidationParameters()\n        member val public ValidationSetId = String.Empty with get, set\n        member val public TargetBranchId = String.Empty with get, set\n        member val public Rules: ValidationSetRule list = [] with get, set\n        member val public Validations: Validation list = [] with get, set\n\n    /// Parameters for /validation-set/delete.\n    type DeleteValidationSetParameters() =\n        inherit ValidationParameters()\n        member val public ValidationSetId = String.Empty with get, set\n        member val public Force = false with get, set\n        member val public DeleteReason = String.Empty with get, set\n\n    /// Parameters for /validation-result/record.\n    type RecordValidationResultParameters() =\n        inherit ValidationParameters()\n        member val public ValidationResultId = String.Empty with get, set\n        member val public ValidationSetId = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n        member val public PromotionSetStepId = String.Empty with get, set\n        member val public StepsComputationAttempt = -1 with get, set\n        member val public ValidationName = String.Empty with get, set\n        member val public ValidationVersion = String.Empty with get, set\n        member val public Status = String.Empty with get, set\n        member val public Summary = String.Empty with get, set\n        member val public ArtifactIds: string array = [||] with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Parameters/WorkItem.Parameters.fs",
    "content": "namespace Grace.Shared.Parameters\n\nopen Grace.Shared.Parameters.Common\nopen System\nopen System.Collections.Generic\n\nmodule WorkItem =\n    /// Base parameters for WorkItem endpoints.\n    type WorkItemParameters() =\n        inherit CommonParameters()\n        member val public OwnerId = String.Empty with get, set\n        member val public OwnerName = String.Empty with get, set\n        member val public OrganizationId = String.Empty with get, set\n        member val public OrganizationName = String.Empty with get, set\n        member val public RepositoryId = String.Empty with get, set\n        member val public RepositoryName = String.Empty with get, set\n        member val public WorkItemId = String.Empty with get, set\n\n    /// Parameters for /work/create.\n    type CreateWorkItemParameters() =\n        inherit WorkItemParameters()\n        member val public Title = String.Empty with get, set\n        member val public Description = String.Empty with get, set\n\n    /// Parameters for /work/{id}.\n    type GetWorkItemParameters() =\n        inherit WorkItemParameters()\n\n    /// Parameters for /work/{id}/update.\n    type UpdateWorkItemParameters() =\n        inherit WorkItemParameters()\n        member val public Title = String.Empty with get, set\n        member val public Description = String.Empty with get, set\n        member val public Status = String.Empty with get, set\n        member val public Constraints = String.Empty with get, set\n        member val public Notes = String.Empty with get, set\n        member val public ArchitecturalNotes = String.Empty with get, set\n        member val public MigrationNotes = String.Empty with get, set\n\n    /// Parameters for /work/{id}/link/reference.\n    type LinkReferenceParameters() =\n        inherit WorkItemParameters()\n        member val public ReferenceId = String.Empty with get, set\n\n    /// Parameters for /work/{id}/link/artifact.\n    type LinkArtifactParameters() =\n        inherit WorkItemParameters()\n        member val public ArtifactId = String.Empty with get, set\n\n    /// Parameters for /work/{id}/link/promotion-set.\n    type LinkPromotionSetParameters() =\n        inherit WorkItemParameters()\n        member val public PromotionSetId = String.Empty with get, set\n\n    /// Parameters for /work/links/list.\n    type GetWorkItemLinksParameters() =\n        inherit WorkItemParameters()\n\n    /// Parameters for /work/attachments/list.\n    type ListWorkItemAttachmentsParameters() =\n        inherit WorkItemParameters()\n\n    /// Parameters for /work/attachments/show.\n    type ShowWorkItemAttachmentParameters() =\n        inherit WorkItemParameters()\n        member val public AttachmentType = String.Empty with get, set\n        member val public Latest = false with get, set\n\n    /// Parameters for /work/attachments/download.\n    type DownloadWorkItemAttachmentParameters() =\n        inherit WorkItemParameters()\n        member val public ArtifactId = String.Empty with get, set\n\n    /// Parameters for /work/links/remove/reference.\n    type RemoveReferenceLinkParameters() =\n        inherit WorkItemParameters()\n        member val public ReferenceId = String.Empty with get, set\n\n    /// Parameters for /work/links/remove/promotion-set.\n    type RemovePromotionSetLinkParameters() =\n        inherit WorkItemParameters()\n        member val public PromotionSetId = String.Empty with get, set\n\n    /// Parameters for /work/links/remove/artifact.\n    type RemoveArtifactLinkParameters() =\n        inherit WorkItemParameters()\n        member val public ArtifactId = String.Empty with get, set\n\n    /// Parameters for /work/links/remove/artifact-type.\n    type RemoveArtifactTypeLinksParameters() =\n        inherit WorkItemParameters()\n        member val public ArtifactType = String.Empty with get, set\n\n    /// Parameters for /work/add-summary.\n    type AddSummaryParameters() =\n        inherit WorkItemParameters()\n        member val public SummaryContent = String.Empty with get, set\n        member val public SummaryMimeType = String.Empty with get, set\n        member val public PromptContent = String.Empty with get, set\n        member val public PromptMimeType = String.Empty with get, set\n        member val public PromptOrigin = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n        member val public SummaryArtifactId = String.Empty with get, set\n        member val public PromptArtifactId = String.Empty with get, set\n\n    /// Result for /work/add-summary.\n    type AddSummaryResult() =\n        member val public WorkItemId = String.Empty with get, set\n        member val public SummaryArtifactId = String.Empty with get, set\n        member val public PromptArtifactId = String.Empty with get, set\n        member val public PromotionSetId = String.Empty with get, set\n\n    /// Attachment metadata for reviewer attachment retrieval endpoints.\n    type WorkItemAttachmentDescriptor() =\n        member val public ArtifactId = String.Empty with get, set\n        member val public AttachmentType = String.Empty with get, set\n        member val public MimeType = String.Empty with get, set\n        member val public Size = 0L with get, set\n        member val public CreatedAt = String.Empty with get, set\n\n    /// Result for /work/attachments/list.\n    type ListWorkItemAttachmentsResult() =\n        member val public WorkItemId = String.Empty with get, set\n        member val public WorkItemNumber = 0L with get, set\n        member val public Attachments = List<WorkItemAttachmentDescriptor>() with get, set\n\n    /// Result for /work/attachments/show.\n    type ShowWorkItemAttachmentResult() =\n        member val public WorkItemId = String.Empty with get, set\n        member val public WorkItemNumber = 0L with get, set\n        member val public AttachmentType = String.Empty with get, set\n        member val public ArtifactId = String.Empty with get, set\n        member val public MimeType = String.Empty with get, set\n        member val public Size = 0L with get, set\n        member val public CreatedAt = String.Empty with get, set\n        member val public IsTextContent = false with get, set\n        member val public Content = String.Empty with get, set\n        member val public AvailableAttachmentCount = 0 with get, set\n        member val public SelectedUsingLatest = false with get, set\n\n    /// Result for /work/attachments/download.\n    type DownloadWorkItemAttachmentResult() =\n        member val public WorkItemId = String.Empty with get, set\n        member val public WorkItemNumber = 0L with get, set\n        member val public AttachmentType = String.Empty with get, set\n        member val public ArtifactId = String.Empty with get, set\n        member val public MimeType = String.Empty with get, set\n        member val public Size = 0L with get, set\n        member val public CreatedAt = String.Empty with get, set\n        member val public DownloadUri = String.Empty with get, set\n"
  },
  {
    "path": "src/Grace.Shared/Resources/Text/Languages.Resources.fs",
    "content": "namespace Grace.Shared.Resources\n\nopen NodaTime\nopen System\nopen Microsoft.FSharp.Reflection\n\nmodule Text =\n\n    /// This is intended to be the definitive list of locali[sz]ations that Grace supports.\n    ///\n    /// I'm starting with en-US, because that's all I know, but trying to do it right from the start.\n    type Language = | ``EN-US``\n\n    type StringResourceName =\n        | AssignIsDisabled\n        | Branch\n        | BranchAlreadyExists\n        | BranchDoesNotExist\n        | BranchIdDoesNotExist\n        | BranchIdIsRequired\n        | BranchIdsAreRequired\n        | BranchIsNotBasedOnLatestPromotion\n        | BranchNameAlreadyExists\n        | BranchNameIsRequired\n        | CheckpointIsDisabled\n        | CommitIsDisabled\n        | CreatingNewDirectoryVersions\n        | CreatingSaveReference\n        | DeleteReasonIsRequired\n        | DescriptionIsRequired\n        | DescriptionIsTooLong\n        | DirectoryAlreadyExists\n        | DirectoryDoesNotExist\n        | DirectorySha256HashAlreadyExists\n        | DuplicateCorrelationId\n        | EitherBranchIdOrBranchNameIsRequired\n        | EitherDirectoryVersionIdOrSha256HashRequired\n        | EitherOrganizationIdOrOrganizationNameIsRequired\n        | EitherOwnerIdOrOwnerNameIsRequired\n        | EitherRepositoryIdOrRepositoryNameIsRequired\n        | EitherToBranchIdOrToBranchNameIsRequired\n        | ExceptionCaught\n        | ExternalIsDisabled\n        | FailedCommunicatingWithObjectStorage\n        | FailedCreatingEmptyDirectoryVersion\n        | FailedCreatingInitialBranch\n        | FailedRebasingInitialBranch\n        | FailedCreatingInitialPromotion\n        | FailedToAddReference\n        | FailedToGetUploadUrls\n        | FailedToRetrieveBranch\n        | FailedUploadingFilesToObjectStorage\n        | FailedWhileApplyingEvent\n        | FailedWhileSavingEvent\n        | FileNotFoundInObjectStorage\n        | FileSha256HashDoesNotMatch\n        | FilesMustNotBeEmpty\n        | GettingLatestVersion\n        | GettingCurrentBranch\n        | GraceConfigFileNotFound\n        | IndexFileNotFound\n        | InitialPromotionMessage\n        | InterprocessFileDeleted\n        | InvalidBranchId\n        | InvalidBranchName\n        | InvalidCheckpointDaysValue\n        | InvalidConflictResolutionPolicy\n        | InvalidDiffCacheDaysValue\n        | InvalidDirectoryVersionCacheDaysValue\n        | InvalidDirectoryPath\n        | InvalidDirectoryVersionId\n        | InvalidLogicalDeleteDaysValue\n        | InvalidMaxCountValue\n        | InvalidNewName\n        | InvalidObjectStorageProvider\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOrganizationType\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidOwnerType\n        | InvalidReferenceId\n        | InvalidReferenceType\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidRepositoryStatus\n        | InvalidSaveDaysValue\n        | InvalidSearchVisibility\n        | InvalidServerApiVersion\n        | InvalidSha256Hash\n        | InvalidSize\n        | InvalidVisibilityValue\n        | PromotionIsDisabled\n        | PromotionNotAvailableBecauseThereAreNoPromotableReferences\n        | MessageIsRequired\n        | NotImplemented\n        | ObjectCacheFileNotFound\n        | ObjectStorageException\n        | Organization\n        | OrganizationIdAlreadyExists\n        | OrganizationNameAlreadyExists\n        | OrganizationContainsRepositories\n        | OrganizationDoesNotExist\n        | OrganizationIdDoesNotExist\n        | OrganizationIdIsRequired\n        | OrganizationIsDeleted\n        | OrganizationIsNotDeleted\n        | OrganizationNameIsRequired\n        | OrganizationTypeIsRequired\n        | Owner\n        | OwnerContainsOrganizations\n        | OwnerDoesNotExist\n        | OwnerIdDoesNotExist\n        | OwnerIdAlreadyExists\n        | OwnerIdIsRequired\n        | OwnerIsDeleted\n        | OwnerIsNotDeleted\n        | OwnerNameAlreadyExists\n        | OwnerNameIsRequired\n        | OwnerTypeIsRequired\n        | ParentBranchDoesNotAllowPromotions\n        | ParentBranchDoesNotExist\n        | ReadingGraceStatus\n        | ReferenceAlreadyExists\n        | ReferenceIdDoesNotExist\n        | ReferenceIdsAreRequired\n        | ReferenceTypeMustBeProvided\n        | RelativePathMustNotBeEmpty\n        | Repository\n        | RepositoryContainsBranches\n        | RepositoryDoesNotExist\n        | RepositoryIdAlreadyExists\n        | RepositoryIdDoesNotExist\n        | RepositoryIdIsRequired\n        | RepositoryIsAlreadyInitialized\n        | RepositoryIsDeleted\n        | RepositoryIsNotDeleted\n        | RepositoryIsNotEmpty\n        | RepositoryNameIsRequired\n        | RepositoryNameAlreadyExists\n        | SaveIsDisabled\n        | SavingDirectoryVersions\n        | ScanningWorkingDirectory\n        | SearchVisibilityIsRequired\n        | ServerRequestsMustIncludeXCorrelationIdHeader\n        | Sha256HashDoesNotExist\n        | Sha256HashDoesNotMatch\n        | Sha256HashIsRequired\n        | StringIsTooLong\n        | TagIsDisabled\n        | TestFailed\n        | UnknownObjectStorageProvider\n        | UpdatingWorkingDirectory\n        | UploadingFiles\n        | ValueMustBePositive\n        | WritingGraceStatusFile\n\n    /// Computes text for how long ago an instant was.\n    ///\n    /// You can pass the two instants in either order. The language code should be a two-letter language code, such as \"en\" or \"es\".\n    let ago language (instant: Instant) =\n        let instant2 = SystemClock.Instance.GetCurrentInstant()\n\n        let since = if instant2 > instant then instant2.Minus(instant) else instant.Minus(instant2)\n\n        let totalSeconds = since.TotalSeconds\n        let totalMinutes = since.TotalMinutes\n        let totalHours = since.TotalHours\n        let totalDays = since.TotalDays\n\n        match language with\n        | \"en\" -> (* English *)\n            if totalSeconds < 2.0 then $\"1 second ago\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} seconds ago\"\n            elif totalMinutes < 2.0 then $\"1 minute ago\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minutes ago\"\n            elif totalHours < 2.0 then $\"1 hour ago\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} hours ago\"\n            elif totalDays < 2.0 then $\"1 day ago\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} days ago\"\n            elif totalDays < 60.0 then $\"1 month ago\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} months ago\"\n            elif totalDays < 730.5 then $\"1 year ago\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} years ago\"\n        | \"zh\" -> (* Chinese *)\n            if totalSeconds < 2.0 then $\"1 秒前\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} 秒前\"\n            elif totalMinutes < 2.0 then $\"1 分钟前\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} 分钟前\"\n            elif totalHours < 2.0 then $\"1 小时前\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} 小时前\"\n            elif totalDays < 2.0 then $\"1 天前\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} 天前\"\n            elif totalDays < 60.0 then $\"1 个月前\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} 个月前\"\n            elif totalDays < 730.5 then $\"1 年前\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} 年前\"\n        | \"jp\" -> (* Japanese *)\n            if totalSeconds < 2.0 then $\"1 秒前\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} 秒前\"\n            elif totalMinutes < 2.0 then $\"1 分前\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} 分前\"\n            elif totalHours < 2.0 then $\"1 時間前\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} 時間前\"\n            elif totalDays < 2.0 then $\"1 日前\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} 日前\"\n            elif totalDays < 60.0 then $\"1 ヶ月前\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} ヶ月前\"\n            elif totalDays < 730.5 then $\"1 年前\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} 年前\"\n        | \"ru\" -> (* Russian *)\n            if totalSeconds < 2.0 then $\"1 секунду назад\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} секунд назад\"\n            elif totalMinutes < 2.0 then $\"1 минуту назад\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} минут назад\"\n            elif totalHours < 2.0 then $\"1 час назад\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} часов назад\"\n            elif totalDays < 2.0 then $\"1 день назад\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} дней назад\"\n            elif totalDays < 60.0 then $\"1 месяц назад\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} месяцев назад\"\n            elif totalDays < 730.5 then $\"1 год назад\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} лет назад\"\n        | \"uk\" -> (* Ukrainian *)\n            if totalSeconds < 2.0 then $\"1 секунду тому\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} секунд тому\"\n            elif totalMinutes < 2.0 then $\"1 хвилину тому\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} хвилин тому\"\n            elif totalHours < 2.0 then $\"1 годину тому\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} годин тому\"\n            elif totalDays < 2.0 then $\"1 день тому\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} днів тому\"\n            elif totalDays < 60.0 then $\"1 місяць тому\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} місяців тому\"\n            elif totalDays < 730.5 then $\"1 рік тому\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} років тому\"\n        | \"hu\" -> (* Hungarian *)\n            if totalSeconds < 2.0 then $\"1 másodperce\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} másodperce\"\n            elif totalMinutes < 2.0 then $\"1 perce\"\n            elif totalMinutes < 60.0 then $\"${Math.Floor(totalMinutes):F0} perce\"\n            elif totalHours < 2.0 then $\"1 órája\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} órája\"\n            elif totalDays < 2.0 then $\"1 napja\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} napja\"\n            elif totalDays < 60.0 then $\"1 hónapja\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} hónapja\"\n            elif totalDays < 730.5 then $\"1 éve\"\n            else $\"${Math.Floor(totalDays / 365.25):F0} éve\"\n        | \"pt\" -> (* Portuguese *)\n            if totalSeconds < 2.0 then $\"1 segundo atrás\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} segundos atrás\"\n            elif totalMinutes < 2.0 then $\"1 minuto atrás\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minutos atrás\"\n            elif totalHours < 2.0 then $\"1 hora atrás\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} horas atrás\"\n            elif totalDays < 2.0 then $\"1 dia atrás\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} dias atrás\"\n            elif totalDays < 60.0 then $\"1 mês atrás\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} meses atrás\"\n            elif totalDays < 730.5 then $\"1 ano atrás\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} anos atrás\"\n        | \"it\" -> (* Italian *)\n            if totalSeconds < 2.0 then $\"1 secondo fa\"\n            elif totalSeconds < 60.0 then $\"fa {Math.Floor(totalSeconds):F0} secondi\"\n            elif totalMinutes < 2.0 then $\"fa 1 minuto\"\n            elif totalMinutes < 60.0 then $\"fa {Math.Floor(totalMinutes):F0} minuti\"\n            elif totalHours < 2.0 then $\"fa 1 ora\"\n            elif totalHours < 24.0 then $\"fa {Math.Floor(totalHours):F0} ore\"\n            elif totalDays < 2.0 then $\"fa 1 giorno\"\n            elif totalDays < 30.0 then $\"fa {Math.Floor(totalDays):F0} giorni\"\n            elif totalDays < 60.0 then $\"fa 1 mese\"\n            elif totalDays < 365.25 then $\"fa {Math.Floor(totalDays / 30.0):F0} mesi\"\n            elif totalDays < 730.5 then $\"fa 1 anno\"\n            else $\"fa {Math.Floor(totalDays / 365.25):F0} anni\"\n        | \"es\" -> (* Spanish *)\n            if totalSeconds < 2.0 then $\"hace 1 segundo\"\n            elif totalSeconds < 60.0 then $\"hace {Math.Floor(totalSeconds):F0} segundos\"\n            elif totalMinutes < 2.0 then $\"hace 1 minuto\"\n            elif totalMinutes < 60.0 then $\"hace {Math.Floor(totalMinutes):F0} minutos\"\n            elif totalHours < 2.0 then $\"hace 1 hora\"\n            elif totalHours < 24.0 then $\"hace {Math.Floor(totalHours):F0} horas\"\n            elif totalDays < 2.0 then $\"hace 1 día\"\n            elif totalDays < 30.0 then $\"hace {Math.Floor(totalDays):F0} días\"\n            elif totalDays < 60.0 then $\"hace 1 mes\"\n            elif totalDays < 365.25 then $\"hace {Math.Floor(totalDays / 30.0):F0} meses\"\n            elif totalDays < 730.5 then $\"hace 1 año\"\n            else $\"hace {Math.Floor(totalDays / 365.25):F0} años\"\n        | \"de\" -> (* German *)\n            if totalSeconds < 2.0 then $\"vor 1 Sekunde\"\n            elif totalSeconds < 60.0 then $\"vor {Math.Floor(totalSeconds):F0} Sekunden\"\n            elif totalMinutes < 2.0 then $\"vor 1 Minute\"\n            elif totalMinutes < 60.0 then $\"vor {Math.Floor(totalMinutes):F0} Minuten\"\n            elif totalHours < 2.0 then $\"vor 1 Stunde\"\n            elif totalHours < 24.0 then $\"vor {Math.Floor(totalHours):F0} Stunden\"\n            elif totalDays < 2.0 then $\"vor 1 Tag\"\n            elif totalDays < 30.0 then $\"vor {Math.Floor(totalDays):F0} Tagen\"\n            elif totalDays < 60.0 then $\"vor 1 Monat\"\n            elif totalDays < 365.25 then $\"vor {Math.Floor(totalDays / 30.0):F0} Monaten\"\n            elif totalDays < 730.5 then $\"vor 1 Jahr\"\n            else $\"vor {Math.Floor(totalDays / 365.25):F0} Jahren\"\n        | \"fr\" -> (* French *)\n            if totalSeconds < 2.0 then $\"il y a 1 seconde\"\n            elif totalSeconds < 60.0 then $\"il y a {Math.Floor(totalSeconds):F0} secondes\"\n            elif totalMinutes < 2.0 then $\"il y a 1 minute\"\n            elif totalMinutes < 60.0 then $\"il y a {Math.Floor(totalMinutes):F0} minutes\"\n            elif totalHours < 2.0 then $\"il y a 1 heure\"\n            elif totalHours < 24.0 then $\"il y a {Math.Floor(totalHours):F0} heures\"\n            elif totalDays < 2.0 then $\"il y a 1 jour\"\n            elif totalDays < 30.0 then $\"il y a {Math.Floor(totalDays):F0} jours\"\n            elif totalDays < 60.0 then $\"il y a 1 mois\"\n            elif totalDays < 365.25 then $\"il y a {Math.Floor(totalDays / 30.0):F0} mois\"\n            elif totalDays < 730.5 then $\"il y a 1 an\"\n            else $\"il y a {Math.Floor(totalDays / 365.25):F0} ans\"\n        | _ -> (* English by default *)\n            if totalSeconds < 2.0 then $\"1 second ago\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} seconds ago\"\n            elif totalMinutes < 2.0 then $\"1 minute ago\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minutes ago\"\n            elif totalHours < 2.0 then $\"1 hour ago\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} hours ago\"\n            elif totalDays < 2.0 then $\"1 day ago\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} days ago\"\n            elif totalDays < 60.0 then $\"1 month ago\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} months ago\"\n            elif totalDays < 730.5 then $\"1 year ago\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} years ago\"\n\n    /// Computes text for how far apart two instants are.\n    let apart language (instant1: Instant) (instant2: Instant) =\n        let since =\n            if instant2 > instant1 then\n                instant2.Minus(instant1)\n            else\n                instant1.Minus(instant2)\n\n        let totalSeconds = since.TotalSeconds\n        let totalMinutes = since.TotalMinutes\n        let totalHours = since.TotalHours\n        let totalDays = since.TotalDays\n\n        match language with\n        | \"en\" -> (* English *)\n            if totalSeconds < 2.0 then $\"1 second apart\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} seconds apart\"\n            elif totalMinutes < 2.0 then $\"1 minute apart\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minutes apart\"\n            elif totalHours < 2.0 then $\"1 hour apart\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} hours apart\"\n            elif totalDays < 2.0 then $\"1 day apart\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} days apart\"\n            elif totalDays < 60.0 then $\"1 month apart\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} months apart\"\n            elif totalDays < 730.5 then $\"1 year apart\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} years apart\"\n        | \"es\" -> (* Spanish *)\n            if totalSeconds < 2.0 then\n                \"1 segundo de diferencia\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} segundos de diferencia\"\n            elif totalMinutes < 2.0 then\n                \"1 minuto de diferencia\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} minutos de diferencia\"\n            elif totalHours < 2.0 then\n                \"1 hora de diferencia\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} horas de diferencia\"\n            elif totalDays < 2.0 then\n                \"1 día de diferencia\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} días de diferencia\"\n            elif totalDays < 60.0 then\n                \"1 mes de diferencia\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} meses de diferencia\"\n            elif totalDays < 730.5 then\n                \"1 año de diferencia\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} años de diferencia\"\n        | \"pt\" -> (* Portuguese *)\n            if totalSeconds < 2.0 then\n                \"1 segundo de diferença\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} segundos de diferença\"\n            elif totalMinutes < 2.0 then\n                \"1 minuto de diferença\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} minutos de diferença\"\n            elif totalHours < 2.0 then\n                \"1 hora de diferença\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} horas de diferença\"\n            elif totalDays < 2.0 then\n                \"1 dia de diferença\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} dias de diferença\"\n            elif totalDays < 60.0 then\n                \"1 mês de diferença\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} meses de diferença\"\n            elif totalDays < 730.5 then\n                \"1 ano de diferença\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} anos de diferença\"\n        | \"de\" -> (* German *)\n            if totalSeconds < 2.0 then\n                \"1 Sekunde auseinander\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} Sekunden auseinander\"\n            elif totalMinutes < 2.0 then\n                \"1 Minute auseinander\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} Minuten auseinander\"\n            elif totalHours < 2.0 then\n                \"1 Stunde auseinander\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} Stunden auseinander\"\n            elif totalDays < 2.0 then\n                \"1 Tag auseinander\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} Tage auseinander\"\n            elif totalDays < 60.0 then\n                \"1 Monat auseinander\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} Monate auseinander\"\n            elif totalDays < 730.5 then\n                \"1 Jahr auseinander\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} Jahre auseinander\"\n        | \"fr\" -> (* French *)\n            if totalSeconds < 2.0 then \"1 seconde d'écart\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} secondes d'écart\"\n            elif totalMinutes < 2.0 then \"1 minute d'écart\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minutes d'écart\"\n            elif totalHours < 2.0 then \"1 heure d'écart\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} heures d'écart\"\n            elif totalDays < 2.0 then \"1 jour d'écart\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} jours d'écart\"\n            elif totalDays < 60.0 then \"1 mois d'écart\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} mois d'écart\"\n            elif totalDays < 730.5 then \"1 an d'écart\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} ans d'écart\"\n        | \"nl\" -> // Dutch\n            if totalSeconds < 2.0 then\n                \"1 seconde verschil\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} seconden verschil\"\n            elif totalMinutes < 2.0 then\n                \"1 minuut verschil\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} minuten verschil\"\n            elif totalHours < 2.0 then\n                \"1 uur verschil\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} uur verschil\"\n            elif totalDays < 2.0 then\n                \"1 dag verschil\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} dagen verschil\"\n            elif totalDays < 60.0 then\n                \"1 maand verschil\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} maanden verschil\"\n            elif totalDays < 730.5 then\n                \"1 jaar verschil\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} jaar verschil\"\n        | \"it\" -> // Italian\n            if totalSeconds < 2.0 then\n                \"1 secondo di differenza\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} secondi di differenza\"\n            elif totalMinutes < 2.0 then\n                \"1 minuto di differenza\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} minuti di differenza\"\n            elif totalHours < 2.0 then\n                \"1 ora di differenza\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} ore di differenza\"\n            elif totalDays < 2.0 then\n                \"1 giorno di differenza\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} giorni di differenza\"\n            elif totalDays < 60.0 then\n                \"1 mese di differenza\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} mesi di differenza\"\n            elif totalDays < 730.5 then\n                \"1 anno di differenza\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} anni di differenza\"\n        | \"hu\" -> // Hungarian\n            if totalSeconds < 2.0 then\n                \"1 másodperc különbség\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} másodperc különbség\"\n            elif totalMinutes < 2.0 then\n                \"1 perc különbség\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} perc különbség\"\n            elif totalHours < 2.0 then\n                \"1 óra különbség\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} óra különbség\"\n            elif totalDays < 2.0 then\n                \"1 nap különbség\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} nap különbség\"\n            elif totalDays < 60.0 then\n                \"1 hónap különbség\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} hónap különbség\"\n            elif totalDays < 730.5 then\n                \"1 év különbség\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} év különbség\"\n        | \"cs\" -> // Czech\n            if totalSeconds < 2.0 then \"1 sekunda rozdíl\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} sekund rozdíl\"\n            elif totalMinutes < 2.0 then \"1 minuta rozdíl\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minut rozdíl\"\n            elif totalHours < 2.0 then \"1 hodina rozdíl\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} hodin rozdíl\"\n            elif totalDays < 2.0 then \"1 den rozdíl\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} dnů rozdíl\"\n            elif totalDays < 60.0 then \"1 měsíc rozdíl\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} měsíců rozdíl\"\n            elif totalDays < 730.5 then \"1 rok rozdíl\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} let rozdíl\"\n        | \"uk\" -> // Ukrainian\n            if totalSeconds < 2.0 then\n                \"1 секунда різниці\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} секунд різниці\"\n            elif totalMinutes < 2.0 then\n                \"1 хвилина різниці\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} хвилин різниці\"\n            elif totalHours < 2.0 then\n                \"1 година різниці\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} годин різниці\"\n            elif totalDays < 2.0 then\n                \"1 день різниці\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} днів різниці\"\n            elif totalDays < 60.0 then\n                \"1 місяць різниці\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} місяців різниці\"\n            elif totalDays < 730.5 then\n                \"1 рік різниці\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} років різниці\"\n        | \"ru\" -> // Russian\n            if totalSeconds < 2.0 then\n                \"1 секунда разницы\"\n            elif totalSeconds < 60.0 then\n                $\"{Math.Floor(totalSeconds):F0} секунд разницы\"\n            elif totalMinutes < 2.0 then\n                \"1 минута разницы\"\n            elif totalMinutes < 60.0 then\n                $\"{Math.Floor(totalMinutes):F0} минут разницы\"\n            elif totalHours < 2.0 then\n                \"1 час разницы\"\n            elif totalHours < 24.0 then\n                $\"{Math.Floor(totalHours):F0} часов разницы\"\n            elif totalDays < 2.0 then\n                \"1 день разницы\"\n            elif totalDays < 30.0 then\n                $\"{Math.Floor(totalDays):F0} дней разницы\"\n            elif totalDays < 60.0 then\n                \"1 месяц разницы\"\n            elif totalDays < 365.25 then\n                $\"{Math.Floor(totalDays / 30.0):F0} месяцев разницы\"\n            elif totalDays < 730.5 then\n                \"1 год разницы\"\n            else\n                $\"{Math.Floor(totalDays / 365.25):F0} лет разницы\"\n        | \"ja\" -> // Japanese\n            if totalSeconds < 2.0 then \"1秒の差\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0}秒の差\"\n            elif totalMinutes < 2.0 then \"1分の差\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0}分の差\"\n            elif totalHours < 2.0 then \"1時間の差\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0}時間の差\"\n            elif totalDays < 2.0 then \"1日の差\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0}日の差\"\n            elif totalDays < 60.0 then \"1ヶ月の差\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0}ヶ月の差\"\n            elif totalDays < 730.5 then \"1年の差\"\n            else $\"{Math.Floor(totalDays / 365.25):F0}年の差\"\n        | \"zh\" -> // Chinese\n            if totalSeconds < 2.0 then \"相差1秒\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0}秒相差\"\n            elif totalMinutes < 2.0 then \"相差1分钟\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0}分钟相差\"\n            elif totalHours < 2.0 then \"相差1小时\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0}小时相差\"\n            elif totalDays < 2.0 then \"相差1天\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0}天相差\"\n            elif totalDays < 60.0 then \"相差1个月\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0}个月相差\"\n            elif totalDays < 730.5 then \"相差1年\"\n            else $\"{Math.Floor(totalDays / 365.25):F0}年相差\"\n        | _ -> (* English by default *)\n            if totalSeconds < 2.0 then $\"1 second apart\"\n            elif totalSeconds < 60.0 then $\"{Math.Floor(totalSeconds):F0} seconds apart\"\n            elif totalMinutes < 2.0 then $\"1 minute apart\"\n            elif totalMinutes < 60.0 then $\"{Math.Floor(totalMinutes):F0} minutes apart\"\n            elif totalHours < 2.0 then $\"1 hour apart\"\n            elif totalHours < 24.0 then $\"{Math.Floor(totalHours):F0} hours apart\"\n            elif totalDays < 2.0 then $\"1 day apart\"\n            elif totalDays < 30.0 then $\"{Math.Floor(totalDays):F0} days apart\"\n            elif totalDays < 60.0 then $\"1 month apart\"\n            elif totalDays < 365.25 then $\"{Math.Floor(totalDays / 30.0):F0} months apart\"\n            elif totalDays < 730.5 then $\"1 year apart\"\n            else $\"{Math.Floor(totalDays / 365.25):F0} years apart\"\n\n    let listCases<'T> () =\n        FSharpType.GetUnionCases typeof<'T>\n        |> Array.map (fun c -> c.Name)\n\n    let listCasesAsString<'T> () =\n        let cases = listCases<'T> () |> String.concat \", \"\n        if cases.Length > 2 then cases[..^2] else String.Empty\n"
  },
  {
    "path": "src/Grace.Shared/Resources/Text/en-US.fs",
    "content": "namespace Grace.Shared.Resources\n\nopen Grace.Shared\nopen Grace.Shared.Resources.Text\nopen System\nopen System.Collections.Generic\nopen System.Linq\nopen Grace.Types\n\nmodule en_US =\n\n    /// <summary>\n    /// Returns the en_US localized string for each value of StringResourceName.\n    /// </summary>\n    /// <param name=\"stringName\">The resource name of the string to return.</param>\n    let getString stringResourceName =\n        match stringResourceName with\n        | AssignIsDisabled -> \"This branch has disabled assign.\"\n        | Branch -> \"Branch\"\n        | BranchAlreadyExists -> \"The branch already exists.\"\n        | BranchDoesNotExist -> \"The branch was not found.\"\n        | BranchIdDoesNotExist -> \"A branch with the provided BranchId does not exist.\"\n        | BranchIdIsRequired -> \"The BranchId must be provided.\"\n        | BranchIdsAreRequired -> \"The list of BranchIds must not be empty.\"\n        | BranchIsNotBasedOnLatestPromotion -> \"The promotion failed because the current branch is not based on the latest promotion from the parent branch.\"\n        | BranchNameAlreadyExists -> \"A branch with the provided BranchName already exists.\"\n        | BranchNameIsRequired -> \"The BranchName must be provided.\"\n        | CheckpointIsDisabled -> \"This branch has disabled checkpoints.\"\n        | CommitIsDisabled -> \"This branch has disabled commits.\"\n        | CreatingNewDirectoryVersions -> \"Creating new directory versions, with updated SHA-256 hashes.\"\n        | CreatingSaveReference -> \"Creating a save reference to mark your current state before switching.\"\n        | DeleteReasonIsRequired -> \"The DeleteReason must be provided.\"\n        | DescriptionIsRequired -> \"The description must be provided.\"\n        | DescriptionIsTooLong -> \"The description is too long.\"\n        | DirectoryAlreadyExists -> \"A directory with the provided DirectoryId already exists.\"\n        | DirectoryDoesNotExist -> \"The directory was not found.\"\n        | DirectorySha256HashAlreadyExists -> \"A directory with the provided SHA-256 hash already exists.\"\n        | DuplicateCorrelationId ->\n            \"The CorrelationId sent was a duplicate of one already used to modify this object. Because this likely indicates a retry of an operation that has already succeeded, this is not allowed.\"\n        | EitherBranchIdOrBranchNameIsRequired -> \"Either a BranchId or a BranchName must be provided. If both are provided, BranchId will be used.\"\n        | EitherDirectoryVersionIdOrSha256HashRequired ->\n            \"Either a DirectoryVersionId or a SHA-256 hash must be provided. If both are provided, DirectoryVersionId will be used.\"\n        | EitherOrganizationIdOrOrganizationNameIsRequired ->\n            \"Either a OrganizationId or a OrganizationName must be provided. If both are provided, OrganizationId will be used.\"\n        | EitherOwnerIdOrOwnerNameIsRequired -> \"Either an OwnerId or an OwnerName must be provided. If both are provided, OwnerId will be used.\"\n        | EitherRepositoryIdOrRepositoryNameIsRequired ->\n            \"Either a RepositoryId, or a RepositoryName, must be provided. If both are provided, RepositoryId will be used.\"\n        | EitherToBranchIdOrToBranchNameIsRequired -> \"Either a ToBranchId or a ToBranchName must be provided. If both are provided, ToBranchId will be used.\"\n        | ExceptionCaught -> \"An exception was caught while processing the request.\"\n        | ExternalIsDisabled -> \"This branch has disabled external references.\"\n        | FailedCommunicatingWithObjectStorage -> \"A failure occurred when communicating with object storage.\"\n        | FailedCreatingEmptyDirectoryVersion -> \"A server error occurred while attempting to create an empty initial directory version.\"\n        | FailedCreatingInitialBranch -> \"A server error occurred while attempting to create the initial branch.\"\n        | FailedCreatingInitialPromotion -> \"A server error occurred while attempting to create the initial promotion.\"\n        | FailedRebasingInitialBranch -> \"A server error occurred while attempting to rebase the initial branch.\"\n        | FailedToAddReference -> \"A server error occurred while attempting to add the reference.\"\n        | FailedToGetUploadUrls -> \"A server error occurred while retrieving the URLs to upload new files to object storage.\"\n        | FailedToRetrieveBranch -> \"A server error occurred while retrieving the branch information.\"\n        | FailedUploadingFilesToObjectStorage -> \"One or more files could not be uploaded to object storage.\"\n        | FailedWhileApplyingEvent -> \"A server error occurred while attempting to update the data transfer object.\"\n        | FailedWhileSavingEvent -> \"A server error occurred while attempting to save the event.\"\n        | FilesMustNotBeEmpty -> \"A non-empty list of files must be provided.\"\n        | FileNotFoundInObjectStorage -> \"The file was not found in object storage.\"\n        | FileSha256HashDoesNotMatch -> \"The provided SHA-256 hash of the file does not match the SHA-256 hash calculated by Grace Server.\"\n        | GettingCurrentBranch -> \"Getting the current branch from the server.\"\n        | GettingLatestVersion -> \"Getting the latest version of the branch you're switching to.\"\n        | GraceConfigFileNotFound ->\n            $\"No {Constants.GraceConfigFileName} file found along current path. Please run `grace config write` if you would like to create one.\"\n        | IndexFileNotFound -> \"The Grace index file was not found. Please run `grace maintenance update-index` to re-create it.\"\n        | InitialPromotionMessage -> \"Initial, empty promotion.\"\n        | InterprocessFileDeleted -> \"Inter-process communication file deleted.\"\n        | InvalidBranchId -> \"The provided BranchId is not a valid Guid.\"\n        | InvalidBranchName ->\n            \"The BranchName is not a valid Grace name. A valid object name in Grace has between 2 and 64 characters, has a letter for the first character ([A-Za-z]), and letters, numbers, or - for the rest ([A-Za-z0-9\\-]{1,63}).\"\n        | InvalidCheckpointDaysValue -> \"The provided value for CheckpointDays is invalid.\"\n        | InvalidConflictResolutionPolicy ->\n            $\"The Conflict Resolution Policy provided is not a valid value. Valid values: {listCasesAsString<Types.ConflictResolutionPolicy> ()}.\"\n        | InvalidDiffCacheDaysValue -> \"The provided value for DiffCacheDays is invalid.\"\n        | InvalidDirectoryVersionCacheDaysValue -> \"The provided value for DirectoryVersionCacheDays is invalid.\"\n        | InvalidDirectoryPath -> \"The provided directory is not a valid directory path.\"\n        | InvalidDirectoryVersionId -> \"The provided DirectoryVersionId is not a valid Guid.\"\n        | InvalidLogicalDeleteDaysValue -> \"The provided value for LogicalDeleteDays is invalid.\"\n        | InvalidMaxCountValue -> \"The provided value for MaxCount is invalid.\"\n        | InvalidNewName ->\n            \"The NewName is not a valid Grace name. A valid object name in Grace has between 2 and 64 characters, has a letter for the first character ([A-Za-z]), and letters, numbers, or - for the rest ([A-Za-z0-9\\-]{1,63}).\"\n        | InvalidObjectStorageProvider -> \"The provided object storage provider is not valid.\"\n        | InvalidOrganizationId -> \"The provided OrganizationId is not a valid Guid.\"\n        | InvalidOrganizationName ->\n            \"The OrganizationName is not a valid Grace name. A valid object name in Grace has between 2 and 64 characters, has a letter for the first character [A-Za-z], and letters, numbers, or - for the rest [A-Za-z0-9\\-]{1,63}.\"\n        | InvalidOrganizationType -> \"The OrganizationType provided is not a valid OrganizationType value.\"\n        | InvalidOwnerId -> \"The provided OwnerId is not a valid Guid.\"\n        | InvalidOwnerName ->\n            \"The OwnerName is not a valid Grace name. A valid object name in Grace has between 2 and 64 characters, has a letter for the first character ([A-Za-z]), and letters, numbers, or - for the rest ([A-Za-z0-9\\-]{1,63}).\"\n        | InvalidOwnerType -> \"The OwnerType provided is not a valid OwnerType value.\"\n        | InvalidReferenceId -> \"The provided ReferenceId is not a valid Guid.\"\n        | InvalidReferenceType -> \"The provided ReferenceType is not valid.\"\n        | InvalidRepositoryId -> \"The provided RepositoryId is not a valid Guid.\"\n        | InvalidRepositoryName ->\n            \"The RepositoryName is not a valid Grace name. A valid object name in Grace has between 2 and 64 characters, has a letter for the first character ([A-Za-z]), and letters, numbers, or - for the rest ([A-Za-z0-9\\-]{1,63}).\"\n        | InvalidRepositoryStatus -> \"The repository status provided is not valid.\"\n        | InvalidSaveDaysValue -> \"The provided value for SaveDays is invalid.\"\n        | InvalidSearchVisibility -> \"The SearchVisibility provided is not a valid SearchVisibility value.\"\n        | InvalidServerApiVersion -> \"The provided ServerApiVersion is not recognized. Please use a published Grace API version identifier.\"\n        | InvalidSha256Hash -> \"The provided SHA-256 hash is not a valid SHA-256 hash value.\"\n        | InvalidSize -> \"The provided size does not match the size calculated by adding the sizes of all files in the directory.\"\n        | InvalidVisibilityValue -> \"The provided visibility value is not valid.\"\n        | PromotionIsDisabled -> \"This branch has disabled promotions.\"\n        | PromotionNotAvailableBecauseThereAreNoPromotableReferences ->\n            \"Promotion is not available because there are no commits or promotions in the current branch to promote to the parent branch.\"\n        | MessageIsRequired -> \"A message is required for this reference.\"\n        | NotImplemented -> \"This feature is not yet implemented.\"\n        | ObjectCacheFileNotFound -> \"The Grace object cache file was not found. Please run `grace maintenance scan` to recreate it.\"\n        | ObjectStorageException -> \"An exception occurred when communicating with the object storage provider.\"\n        | Organization -> \"Organization\"\n        | OrganizationIdAlreadyExists -> \"An Organization with the provided OrganizationId already exists.\"\n        | OrganizationNameAlreadyExists -> \"An organization with the same name and owner already exists.\"\n        | OrganizationContainsRepositories ->\n            \"The organization cannot be deleted because it contains active repositories. Specify --force if you would like to recursively delete all repositories.\"\n        | OrganizationDoesNotExist -> \"The organization was not found.\"\n        | OrganizationIdDoesNotExist -> \"An Organization with the provided OrganizationId does not exist.\"\n        | OrganizationIdIsRequired -> \"The OrganizationId must be provided.\"\n        | OrganizationIsDeleted -> \"The organization is deleted.\"\n        | OrganizationIsNotDeleted -> \"The organization is not deleted.\"\n        | OrganizationNameIsRequired -> \"The OrganizationName must be provided.\"\n        | OrganizationTypeIsRequired -> \"The OrganizationType must be provided.\"\n        | Owner -> \"Owner\"\n        | OwnerContainsOrganizations ->\n            \"The owner cannot be deleted because it contains active organizations. Specify --force if you would like to recursively delete all organizations, and their repositories.\"\n        | OwnerDoesNotExist -> \"The owner was not found.\"\n        | OwnerIdAlreadyExists -> \"An Owner with the provided OwnerId already exists.\"\n        | OwnerIdDoesNotExist -> \"An Owner with the provided OwnerId does not exist.\"\n        | OwnerIdIsRequired -> \"The OwnerId must be provided.\"\n        | OwnerIsDeleted -> \"The owner is deleted.\"\n        | OwnerIsNotDeleted -> \"The owner is not deleted.\"\n        | OwnerNameAlreadyExists -> \"An Owner with the provided OwnerName already exists.\"\n        | OwnerNameIsRequired -> \"The OwnerName must be provided.\"\n        | OwnerTypeIsRequired -> \"The OwnerType must be provided.\"\n        | ParentBranchDoesNotAllowPromotions -> \"The parent branch must allow promotions.\"\n        | ParentBranchDoesNotExist -> \"The parent branch provided does not exist.\"\n        | ReadingGraceStatus -> \"Reading the Grace status file.\"\n        | ReferenceAlreadyExists -> \"A reference with this ReferenceId already exists.\"\n        | ReferenceIdDoesNotExist -> \"The given ReferenceId does not exist.\"\n        | ReferenceIdsAreRequired -> \"The list of ReferenceIds must not be empty.\"\n        | ReferenceTypeMustBeProvided -> \"The reference type cannot be an empty string.\"\n        | RelativePathMustNotBeEmpty -> \"The relative path of the directory cannot be an empty string.\"\n        | Repository -> \"Repository\"\n        | RepositoryContainsBranches ->\n            \"The repository cannot be deleted because it contains active branches. Specify --force if you would like to recursively delete all branches.\"\n        | RepositoryDoesNotExist -> \"The repository was not found.\"\n        | RepositoryIdAlreadyExists -> \"A repository with the provided RepositoryId already exists.\"\n        | RepositoryIdDoesNotExist -> \"A repository with the provided RepositoryId does not exist.\"\n        | RepositoryIdIsRequired -> \"The RepositoryId must be provided.\"\n        | RepositoryIsAlreadyInitialized -> \"The repository is already initialized.\"\n        | RepositoryIsDeleted -> \"The repository is deleted.\"\n        | RepositoryIsNotDeleted -> \"The repository is not deleted.\"\n        | RepositoryIsNotEmpty -> \"The repository is not empty. Only empty repositories can be initialized.\"\n        | RepositoryNameIsRequired -> \"The RepositoryName must be provided.\"\n        | RepositoryNameAlreadyExists -> \"A repository with the provided name already exists.\"\n        | SaveIsDisabled -> \"This branch has disabled saves.\"\n        | SavingDirectoryVersions -> \"Saving the new directory versions on the server.\"\n        | ScanningWorkingDirectory -> \"Scanning your working directory for changes.\"\n        | SearchVisibilityIsRequired -> \"The SearchVisibility must be provided.\"\n        | ServerRequestsMustIncludeXCorrelationIdHeader ->\n            \"Grace requires every server request to include an X-Correlation-Id header. This header should contain a unique string for each call.\"\n        | Sha256HashDoesNotExist -> \"The Sha256Hash value was not found.\"\n        | Sha256HashDoesNotMatch -> \"The provided SHA-256 hash for this directory version does not match the SHA-256 hash calculated by Grace Server.\"\n        | Sha256HashIsRequired -> \"The Sha256Hash value is required.\"\n        | StringIsTooLong -> \"The provided string is longer than allowed.\"\n        | TagIsDisabled -> \"This branch has disabled tags.\"\n        | TestFailed -> \"The test failed.\"\n        | UnknownObjectStorageProvider -> \"The object storage provider for this repository is unknown.\"\n        | UpdatingWorkingDirectory -> \"Updating your working directory to match the new branch.\"\n        | UploadingFiles -> \"Uploading new and updated files to object storage.\"\n        | ValueMustBePositive -> \"The value must be positive.\"\n        | WritingGraceStatusFile -> \"Writing Grace status file.\"\n"
  },
  {
    "path": "src/Grace.Shared/Resources/Utilities.Resources.fs",
    "content": "namespace Grace.Shared.Resources\n\nopen Grace.Shared.Resources.en_US\n\nmodule Utilities =\n\n    /// Retrieves the localized version of a system resource string.\n    ///\n    /// Note: For now, it's hardcoded to return en_US. I'll fix this when we really implement localization.\n    let getLocalizedString stringName = en_US.getString stringName\n"
  },
  {
    "path": "src/Grace.Shared/ReviewNotes.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Review\nopen Grace.Types.Types\nopen NodaTime\nopen System\nopen System.Security.Cryptography\nopen System.Text\n\nmodule ReviewNotes =\n    let private normalizePath (relativePath: RelativePath) = relativePath.Replace('\\\\', '/')\n\n    let private getChapterKey (relativePath: RelativePath) =\n        let normalized = normalizePath relativePath\n        let segments = normalized.Split([| '/' |], StringSplitOptions.RemoveEmptyEntries)\n\n        if segments.Length = 0 then normalized else segments[0]\n\n    let private computeChapterId (paths: RelativePath list) =\n        let signature = paths |> List.sort |> String.concat \"|\"\n        let bytes = Encoding.UTF8.GetBytes(signature)\n        let hashBytes = SHA256.HashData(bytes)\n        Sha256Hash(byteArrayToString hashBytes)\n\n    let buildChapters (paths: RelativePath list) (evidence: EvidenceSliceSummary list) =\n        paths\n        |> List.groupBy getChapterKey\n        |> List.sortBy fst\n        |> List.map (fun (chapterKey, chapterPaths) ->\n            let orderedPaths = chapterPaths |> List.distinct |> List.sort\n\n            let chapterEvidence =\n                evidence\n                |> List.filter (fun slice -> orderedPaths |> List.contains slice.RelativePath)\n\n            {\n                ChapterId = computeChapterId orderedPaths\n                Title = chapterKey\n                Summary = String.Empty\n                Paths = orderedPaths\n                FindingIds = []\n                Evidence = chapterEvidence\n            })\n\n    let assembleNotes\n        (reviewNotesId: ReviewNotesId)\n        (ownerId: OwnerId)\n        (organizationId: OrganizationId)\n        (repositoryId: RepositoryId)\n        (promotionSetId: PromotionSetId option)\n        (policySnapshotId: PolicySnapshotId)\n        (riskProfile: DeterministicRiskProfile option)\n        (evidenceSummary: EvidenceSetSummary option)\n        (createdAt: Instant)\n        =\n        let paths =\n            riskProfile\n            |> Option.map (fun profile ->\n                profile.ChangedPaths\n                |> List.map (fun path -> path.RelativePath))\n            |> Option.defaultValue []\n\n        let evidence =\n            evidenceSummary\n            |> Option.map (fun summary -> summary.SliceSummaries)\n            |> Option.defaultValue []\n\n        { ReviewNotes.Default with\n            ReviewNotesId = reviewNotesId\n            OwnerId = ownerId\n            OrganizationId = organizationId\n            RepositoryId = repositoryId\n            PromotionSetId = promotionSetId\n            PolicySnapshotId = policySnapshotId\n            Chapters = buildChapters paths evidence\n            EvidenceSetSummary = evidenceSummary\n            CreatedAt = createdAt\n        }\n"
  },
  {
    "path": "src/Grace.Shared/Services.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Microsoft.Extensions.Logging\nopen Microsoft.Extensions.ObjectPool\nopen System\nopen System.Buffers\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.IO\nopen System.Linq\nopen System.Security.Cryptography\nopen System.Text\nopen System.Threading.Tasks\n\nmodule Services =\n\n    /// Adds a property to a GraceResult instance.\n    let enhance<'T> key value (result: GraceResult<'T>) =\n        if not <| String.IsNullOrEmpty(key) then\n            let safeValue = if String.IsNullOrEmpty(value) then String.Empty else value\n\n            match result with\n            | Ok result ->\n                result.Properties[ key ] <- safeValue\n                Ok result\n            | Error error ->\n                error.Properties[ key ] <- safeValue\n                Error error\n        else\n            result\n\n    /// A custom PooledObjectPolicy for IncrementalHash.\n    type IncrementalHashPolicy() =\n        interface IPooledObjectPolicy<IncrementalHash> with\n            member this.Create() = IncrementalHash.CreateHash(HashAlgorithmName.SHA256)\n\n            member this.Return(hashInstance: IncrementalHash) =\n                // Reset the hash instance so it can be reused.\n                // We're calling GetHashAndReset() to reset - there's no Reset() function.\n                let throwaway = stackalloc<byte> SHA256.HashSizeInBytes\n                hashInstance.GetHashAndReset(throwaway) |> ignore\n                true // Indicates that the object is okay to be returned to the pool\n\n    /// An ObjectPool for IncrementalHash instances.\n    let incrementalHashPool =\n        DefaultObjectPoolProvider()\n            .Create(IncrementalHashPolicy())\n\n    /// The 0x00 character.\n    let nulChar = char (0)\n\n    /// Checks if a file is a binary file by scanning the first 8K for a 0x00 character; if it finds one, we assume the file is binary.\n    ///\n    /// This is the same algorithm used by Git.\n    let isBinaryFile (stream: Stream) =\n        task {\n            //logToConsole $\"In isBinaryFile: stream.Length: {stream.Length}.\"\n            // If the file is smaller than 8K, we'll check the whole file.\n            let defaultBytesToCheck = 8 * 1024\n\n            let bytesToCheck =\n                if stream.Length > defaultBytesToCheck then\n                    defaultBytesToCheck\n                else\n                    int (stream.Length)\n\n            // Get a buffer to hold the part of the file we're going to check.\n            let startingBytes = ArrayPool<byte>.Shared.Rent (bytesToCheck)\n            //logToConsole $\"In isBinaryFile: stream.Length: {stream.Length}. Rented byte array of length {bytesToCheck}.\"\n\n            try\n                try\n                    // Read the beginning of the file into the buffer.\n                    //logToConsole $\"In isBinaryFile: stream.Length: {stream.Length}. About to read stream.\"\n                    stream.Position <- 0L\n                    let! bytesRead = stream.ReadAsync(startingBytes, 0, bytesToCheck)\n                    //logToConsole $\"In isBinaryFile: stream.Length: {stream.Length}. Finished reading stream.\"\n\n                    // Search for a 0x00 character.\n                    return\n                        startingBytes\n                            .Take(bytesRead)\n                            .Any(fun b -> char (b) = nulChar)\n                with\n                | ex ->\n                    //logToConsole $\"In isBinaryFile: stream.Length: {stream.Length}. Caught exception: {ExceptionResponse.Create ex}.\"\n                    return false\n            finally\n                // Return the rented buffer to the pool, even if an exception is thrown.\n                if not <| isNull startingBytes then\n                    ArrayPool<byte>.Shared.Return (startingBytes)\n        }\n\n    /// Computes the SHA-256 value for a given file, presented as a stream.\n    ///\n    /// Sha256Hash values for files are computed by hashing the file's contents.\n    let computeSha256ForFile (stream: Stream) (relativeFilePath: RelativePath) =\n        task {\n            // I did some informal perf testing on large files. This size was best, larger didn't help, and 64K keeps it on the small object heap.\n            let bufferLength = 64 * 1024\n\n            // Using object pooling for both of these.\n            let buffer = ArrayPool<byte>.Shared.Rent (bufferLength)\n            let hasher = incrementalHashPool.Get()\n\n            try\n                // Read bytes from the file and feed them into the hasher.\n                let mutable moreToRead = true\n\n                while moreToRead do\n                    let! bytesRead = stream.ReadAsync(buffer, 0, bufferLength)\n\n                    if bytesRead > 0 then\n                        hasher.AppendData(buffer, 0, bytesRead)\n                    else\n                        moreToRead <- false\n\n                // Get the SHA-256 hash as a byte array.\n                let sha256Bytes = stackalloc<byte> SHA256.HashSizeInBytes\n                hasher.GetHashAndReset(sha256Bytes) |> ignore\n\n                // Convert the SHA-256 value from a byte[] to a string, and return it.\n                //    Example: byte[]{0x43, 0x2a, 0x01, 0xfa} -> \"432a01fa\"\n\n                let sha256Hash = byteArrayToString (sha256Bytes)\n\n                return Sha256Hash sha256Hash\n            finally\n                if not <| isNull buffer then\n                    ArrayPool<byte>.Shared.Return (buffer, clearArray = true)\n\n                if not <| isNull hasher then incrementalHashPool.Return(hasher)\n        }\n\n    /// Computes the SHA-256 value for a given relative directory.\n    ///\n    /// Sha256Hash values for directories are computed by concatenating the relative path of the directory, and the Sha256Hash values of all subdirectories and files.\n    let computeSha256ForDirectory (relativeDirectoryPath: RelativePath) (directories: List<LocalDirectoryVersion>) (files: List<LocalFileVersion>) =\n        let hasher = incrementalHashPool.Get()\n\n        try\n            hasher.AppendData(Encoding.UTF8.GetBytes(relativeDirectoryPath))\n\n            // We're sorting just to get consistent ordering; inconsistent ordering would produce difference SHA-256 hashes.\n            let sortedDirectories =\n                directories\n                |> Seq.sortBy (fun subdirectory -> subdirectory.RelativePath)\n\n            for subdirectory in sortedDirectories do\n                hasher.AppendData(Encoding.UTF8.GetBytes(subdirectory.Sha256Hash))\n\n            // Again, sorting to ensure consistent ordering.\n            let sortedFiles =\n                files\n                |> Seq.sortBy (fun file -> file.RelativePath)\n\n            for file in sortedFiles do\n                hasher.AppendData(Encoding.UTF8.GetBytes(file.Sha256Hash))\n\n            // Get the SHA-256 hash as a byte array.\n            let sha256Bytes = stackalloc<byte> SHA256.HashSizeInBytes\n            hasher.GetHashAndReset(sha256Bytes) |> ignore\n\n            // Convert the SHA-256 value from a byte[] to a string, and return it.\n            //   Example: byte[]{0x43, 0x2a, 0x01, 0xfa} -> \"432a01fa\"\n            Sha256Hash(byteArrayToString sha256Bytes)\n        finally\n            if not <| isNull hasher then incrementalHashPool.Return(hasher)\n\n    /// Gets the total size of the files contained within this specific directory. This does not include the size of any subdirectories.\n    let getDirectorySize (files: IList<FileVersion>) =\n        files\n        |> Seq.fold (fun (size: int64) file -> size + file.Size) 0L\n\n    /// Gets the total size of the files contained within this specific directory. This does not include the size of any subdirectories.\n    let getLocalDirectorySize (files: IList<LocalFileVersion>) =\n        files\n        |> Seq.fold (fun (size: int64) file -> size + file.Size) 0L\n\n    /// Gets the number of path segments for the longest relative path in GraceIndex.\n    ///\n    /// For example, \"/src/Grace.Shared/Services.Shared.fs\" has 3 path segments.\n    let getLongestRelativePath (directoryVersions: IEnumerable<LocalDirectoryVersion>) =\n        if directoryVersions |> Seq.isEmpty then\n            0\n        else\n            //logToConsole $\"In getLongestRelativePath:\"\n            //graceStatus.Index.Values |> Seq.iter(fun localDirectoryVersion -> logToConsole $\"  localDirectoryVersion.RelativePath: {localDirectoryVersion.RelativePath}; DirectoryId: {localDirectoryVersion.DirectoryId}\")\n            Math.Max(30, directoryVersions.Max(fun localDirectoryVersion -> localDirectoryVersion.RelativePath.Length))\n"
  },
  {
    "path": "src/Grace.Shared/Utilities.Shared.fs",
    "content": "namespace Grace.Shared\n\nopen Microsoft.Extensions.Caching.Memory\nopen Microsoft.FSharp.NativeInterop\nopen Microsoft.FSharp.Reflection\nopen NodaTime\nopen NodaTime.Text\nopen System\nopen System.Collections.Generic\nopen System.Globalization\nopen System.IO\nopen System.Net.Http.Json\nopen System.Reflection\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\nopen System.Net.Http\nopen System.Net.Security\nopen System.Net\nopen System\nopen System.Reflection\nopen System.Collections.Concurrent\nopen System.Buffers\nopen Microsoft.Extensions.ObjectPool\n\n#nowarn \"9\"\n\nmodule Combinators =\n    let either okFunc errorFunc graceResult =\n        match graceResult with\n        | Result.Ok s -> okFunc s\n        | Result.Error f -> errorFunc f\n\n    let ok x = Result.Ok x\n    let error x = Result.Error x\n\n    let bind f = either f error\n\n    let (>>=) x f = bind f x\n\n    let (>=>) s1 s2 = s1 >> bind s2\n\nmodule Utilities =\n    let memoryCacheOptions = MemoryCacheOptions(TrackStatistics = false, TrackLinkedCacheEntries = false, ExpirationScanFrequency = TimeSpan.FromSeconds(30.0))\n    let memoryCache: IMemoryCache = new MemoryCache(memoryCacheOptions)\n\n    /// Defines a PooledObjectPolicy specialized for the StringBuilder type.\n    type StringBuilderPooledObjectPolicy() =\n        inherit PooledObjectPolicy<StringBuilder>()\n\n        override _.Create() = new StringBuilder()\n\n        override _.Return(sb: StringBuilder) =\n            sb.Clear() |> ignore\n            true\n\n    let pooledObjectPolicy = StringBuilderPooledObjectPolicy()\n\n    /// An ObjectPool that can be used to efficiently get StringBuilder instances.\n    let stringBuilderPool = ObjectPool.Create<StringBuilder>(pooledObjectPolicy)\n\n    /// Gets the current instant.\n    let getCurrentInstant () = SystemClock.Instance.GetCurrentInstant()\n\n    /// Gets a future instant by adding the provided duration to the current time.\n    let getFutureInstant (duration: Duration) = getCurrentInstant().Plus(duration)\n\n    /// Formats an instant as a string in ExtendedIso format.\n    ///\n    /// Example: \"2019-06-15T13:45:30.9040833Z\".\n    let formatInstantExtended (instant: Instant) =\n        let instantString = instant.ToString(InstantPattern.ExtendedIso.PatternText, CultureInfo.InvariantCulture)\n\n        if instantString.Length = 28 then\n            instantString\n        else\n            // Pad the fractional seconds with zeros.\n            let zerosToAdd = 28 - instantString.Length\n            let extraZeros = String.replicate zerosToAdd \"0\"\n            $\"{instantString.Substring(0, instantString.Length - 1)}{extraZeros}Z\"\n\n    //$\"{instant.ToString(InstantPattern.ExtendedIso.PatternText, CultureInfo.InvariantCulture),-28}\"\n\n    /// Gets the current instant as a string in ExtendedIso format.\n    ///\n    /// Example: \"2031-06-15T13:45:30.9040833Z\".\n    let getCurrentInstantExtended () = getCurrentInstant () |> formatInstantExtended\n\n    /// Formats an instant as a string in General format.\n    ///\n    /// Example: \"2019-06-15T13:45:30Z\".\n    let formatInstantGeneral (instant: Instant) = instant.ToString(InstantPattern.General.PatternText, CultureInfo.InvariantCulture)\n\n    /// Gets a future Instant, by adding the provided duration to the current time, and outputs it as a string in ExtendedIso format.\n    ///\n    /// Example output: \"2024-06-01T08:27:04.3273839Z\"\n    let formatFutureInstantExtended (duration: Duration) = formatInstantExtended (getCurrentInstant().Plus(duration))\n\n    /// Gets the current instant as a string in General format.\n    ///\n    /// Example: \"2019-06-15T13:45:30Z\".\n    let getCurrentInstantGeneral () = getCurrentInstant () |> formatInstantGeneral\n\n    /// Converts an Instant to local time, and produces a string in short date/time format, using the CurrentUICulture.\n    let instantToLocalTime (instant: Instant) =\n        instant\n            .ToDateTimeUtc()\n            .ToLocalTime()\n            .ToString(\"g\", CultureInfo.CurrentUICulture)\n\n    /// Gets the current instant in local time as a string in short date/time format, using the CurrentUICulture.\n    let getCurrentInstantLocal () = getCurrentInstant () |> instantToLocalTime\n\n    /// Ensures that the DateTime is printed in exactly the same number of characters, so the output is aligned.\n    let formatDateTimeAligned (dateTime: DateTime) =\n        let datePart = dateTime.ToString(\"ddd yyyy-MM-dd\")\n        let timePart = dateTime.ToString(\"h:mm:ss tt\")\n        let formattedTimePart = if timePart.Length = 10 then \" \" + timePart else timePart\n        sprintf \"%s %s\" datePart formattedTimePart\n\n    /// Ensures that the Instant is printed in exactly the same number of characters, so the output is aligned.\n    let formatInstantAligned (instant: Instant) = formatDateTimeAligned (instant.ToDateTimeUtc())\n\n    let mutable lockObject = new Threading.Lock()\n\n    /// Logs the message to the console, with the current instant and thread ID.\n    let logToConsole message = lock lockObject (fun () -> printfn $\"{getCurrentInstantExtended ()} {Environment.CurrentManagedThreadId:X2} {message}\")\n\n    /// Converts an environment variable name to a key used to look up the value in IConfiguration.\n    let getConfigKey (environmentVariableName: string) = environmentVariableName.Replace(\"__\", \":\")\n\n    /// Gets the elapsed time since the start time, in milliseconds, right-aligned in a string of not less than 7 characters.\n    ///\n    /// If the duration is less than 7 characters, it is padded with spaces.\n    /// If the duration is more than 7 characters, nothing is truncated.\n    let getDurationRightAligned_ms (time: Instant) =\n        let milliseconds = $\"{getCurrentInstant().Minus(time).TotalMilliseconds:F3}\"\n\n        let result =\n            (String.replicate (Math.Max(7 - milliseconds.Length, 0)) \" \")\n            + milliseconds // Right-align, 7 characters.\n\n        result\n\n    /// Gets the first eight characters of a SHA256 hash.\n    let getShortSha256Hash (sha256Hash: String) = if sha256Hash.Length >= 8 then sha256Hash.Substring(0, 8) else String.Empty\n\n    /// Converts both the type name and case name of a discriminated union to a string.\n    ///\n    /// Example: Animal.Dog -> \"Animal.Dog\"\n    let getDiscriminatedUnionFullName (x: 'T) =\n        let discriminatedUnionType = typeof<'T>\n        let (case, _) = FSharpValue.GetUnionFields(x, discriminatedUnionType)\n        $\"{discriminatedUnionType.Name}.{case.Name}\"\n\n    /// Converts just the case name of a discriminated union to a string.\n    ///\n    /// Example: Animal.Dog -> \"Dog\"\n    let getDiscriminatedUnionCaseName (x: 'T) =\n        let discriminatedUnionType = typeof<'T>\n        let (case, _) = FSharpValue.GetUnionFields(x, discriminatedUnionType)\n        $\"{case.Name}\"\n\n    let defaultForType (t: Type) : obj = if t.IsValueType then Activator.CreateInstance t else null\n\n    /// Converts a string into the corresponding case of a discriminated union type.\n    ///\n    /// If the case has fields, they will be initialized to their default values (null for reference types, zero/false for value types).\n    ///\n    /// Examples:\n    ///\n    ///   discriminatedUnionFromString<Animal> \"Dog\" -> Animal.Dog\n    ///\n    ///   discriminatedUnionFromString<Status> \"InProgress\" (where InProgress has a PercentDone field) -> Status.InProgress 0\n    let discriminatedUnionFromString<'T> (s: string) =\n        match FSharpType.GetUnionCases typeof<'T>\n              |> Array.filter (fun case -> String.Compare(case.Name, s, ignoreCase = true) = 0)\n            with\n        | [| case |] ->\n            let fieldCount = case.GetFields().Length\n\n            if fieldCount = 0 then\n                Some(FSharpValue.MakeUnion(case, [||]) :?> 'T)\n            else\n                let fields = case.GetFields()\n                let unionCaseParameters = List<objnull>(fieldCount)\n\n                for i in 0 .. fieldCount - 1 do\n                    let propertyType = fields[i].PropertyType\n                    unionCaseParameters.Add(defaultForType (propertyType))\n\n                Some(FSharpValue.MakeUnion(case, unionCaseParameters.ToArray()) :?> 'T)\n        | _ -> None\n\n    /// Gets the cases of a discriminated union as an array of strings.\n    ///\n    /// Example: listCases<Animal> -> [| \"Dog\"; \"Cat\" |]\n    let listCases<'T> () =\n        FSharpType.GetUnionCases typeof<'T>\n        |> Array.map (fun c -> c.Name)\n\n    /// Gets the cases of discriminated union for serialization.\n    let GetKnownTypes<'T> () =\n        typeof<'T>.GetNestedTypes (BindingFlags.Public ||| BindingFlags.NonPublic)\n        |> Array.filter FSharpType.IsUnion\n\n    /// Serializes an object to JSON, using Grace's custom JsonSerializerOptions.\n    let serialize<'T> item = JsonSerializer.Serialize<'T>(item, Constants.JsonSerializerOptions)\n\n    /// Serializes an object to JSON and writes it to a stream, using Grace's custom JsonSerializerOptions.\n    let serializeAsync<'T> (stream: Stream) item = task { return! JsonSerializer.SerializeAsync<'T>(stream, item, Constants.JsonSerializerOptions) }\n\n    /// Deserializes a JSON string to a provided type, using Grace's custom JsonSerializerOptions.\n    let deserialize<'T> (s: string) = JsonSerializer.Deserialize<'T>(s, Constants.JsonSerializerOptions)\n\n    /// Deserializes a stream of JSON to a provided type, using Grace's custom JsonSerializerOptions.\n    let deserializeAsync<'T> (stream: Stream) = task { return! JsonSerializer.DeserializeAsync<'T>(stream, Constants.JsonSerializerOptions) }\n\n    /// Deserializes the Content from an HttpResponseMessage to the provided type, using Grace's custom JsonSerializerOptions.\n    let deserializeContent<'T> (response: HttpResponseMessage) =\n        task {\n            let! stream = response.Content.ReadAsStreamAsync()\n            return! deserializeAsync<'T> stream\n        }\n\n    /// Create JsonContent from the provided object, using Grace's custom JsonSerializerOptions.\n    let createJsonContent<'T> item = JsonContent.Create(item, options = Constants.JsonSerializerOptions)\n\n    /// Returns true if Grace is running on a Windows machine.\n    let runningOnWindows =\n        match Environment.OSVersion.Platform with\n        | PlatformID.Win32NT\n        | PlatformID.Win32S\n        | PlatformID.Win32Windows\n        | PlatformID.WinCE -> true\n        | _ -> false\n\n    /// Returns true if Grace is running on a MacOS machine.\n    let runningOnMacOS =\n        match Environment.OSVersion.Platform with\n        | PlatformID.MacOSX -> true\n        | _ -> false\n\n    /// Returns true if Grace is running on a Unix or Linux machine.\n    let runningOnLinux =\n        match Environment.OSVersion.Platform with\n        | PlatformID.Unix -> true\n        | _ -> false\n\n    /// Returns true if Grace is running in a browser.\n    let runningOnBrowser =\n        match Environment.OSVersion.Platform with\n        | PlatformID.Other -> true\n        | _ -> false\n\n    /// Returns the given path, replacing any Windows-style backslash characters (\\) with forward-slash characters (/).\n    let normalizedTimeSpan = TimeSpan.FromMinutes(1.0)\n\n    /// Converts backslashes to forward slashes, and caches the result for performance.\n    let normalizeFilePath (filePath: string) =\n        let mutable result = String.Empty\n\n        if not <| memoryCache.TryGetValue(filePath, &result) then\n            let normalized = filePath.Replace(@\"\\\", \"/\")\n\n            memoryCache.Set(filePath, normalized, normalizedTimeSpan)\n            |> ignore\n\n            normalized\n        else\n            result\n\n    /// Switches \"/\" to \"\\\" when we're running on Windows.\n    let getNativeFilePath (filePath: string) = if runningOnWindows then filePath.Replace(\"/\", @\"\\\") else filePath\n\n    /// File stream options for reading files efficiently in Grace.\n    let fileStreamOptionsRead =\n        FileStreamOptions(\n            BufferSize = 8 * 1024,\n            Mode = FileMode.Open,\n            Access = FileAccess.Read,\n            Share = FileShare.Read,\n            Options =\n                (FileOptions.Asynchronous\n                 ||| FileOptions.SequentialScan)\n        )\n\n    /// File stream options for writing files efficiently in Grace.\n    let fileStreamOptionsWrite =\n        FileStreamOptions(BufferSize = 8 * 1024, Mode = FileMode.Create, Access = FileAccess.Write, Share = FileShare.None, Options = FileOptions.Asynchronous)\n\n    /// Returns the directory of a file, relative to the root of the repository's working directory.\n    let getRelativeDirectory (filePath: string) rootDirectory =\n        let standardizedFilePath = normalizeFilePath filePath\n        let standardizedRootDirectory = normalizeFilePath rootDirectory\n        //logToConsole $\"In getRelativeDirectory: standardizedFilePath: {standardizedFilePath}; standardizedRootDirectory: {standardizedRootDirectory}.\"\n        //let originalFileRelativePath =\n        //    if String.IsNullOrEmpty(standardizedRootDirectory) then standardizedFilePath else Path.GetRelativePath(standardizedRootDirectory, standardizedFilePath)\n        //logToConsole $\"originalFileRelativePath: {originalFileRelativePath}.\"\n        let relativePathParts = standardizedFilePath.Split(\"/\")\n        //logToConsole $\"relativePathParts.Length: {relativePathParts.Length}\"\n        if relativePathParts.Length = 1 then\n            Constants.RootDirectoryPath\n        else\n            let sb = stringBuilderPool.Get()\n\n            try\n                let relativeDirectoryPath =\n                    relativePathParts[0..^1]\n                    |> Array.fold (fun (sb: StringBuilder) currentPart -> sb.Append($\"{currentPart}/\")) sb\n\n                relativeDirectoryPath.Remove(relativeDirectoryPath.Length - 1, 1)\n                |> ignore // Remove trailing slash.\n                //logToConsole $\"relativeDirectoryPath.ToString(): {relativeDirectoryPath.ToString()}\"\n                (relativeDirectoryPath.ToString())\n            finally\n                stringBuilderPool.Return(sb)\n\n    /// Returns the directory of a file, relative to the root of the repository's working directory.\n    let getLocalRelativeDirectory (filePath: string) rootDirectory =\n        let standardizedFilePath = normalizeFilePath filePath\n        let standardizedRootDirectory = normalizeFilePath rootDirectory\n        //logToConsole $\"In getRelativeDirectory: standardizedRootDirectory: {standardizedFilePath}; standardizedRootDirectory: {standardizedRootDirectory}.\"\n        let originalFileRelativePath =\n            if String.IsNullOrEmpty(standardizedRootDirectory) then\n                standardizedFilePath\n            else\n                Path.GetRelativePath(standardizedRootDirectory, standardizedFilePath)\n        //logToConsole $\"In getRelativeDirectory: originalFileRelativePath: {originalFileRelativePath}\"\n        let relativePathParts = originalFileRelativePath.Split(Path.DirectorySeparatorChar)\n        //logToConsole $\"In getRelativeDirectory: relativePathParts.Length: {relativePathParts.Length}; relativePathParts[0]: {relativePathParts[0]}\"\n        if relativePathParts.Length = 1\n           && relativePathParts[0] = Constants.RootDirectoryPath then\n            Constants.RootDirectoryPath\n        else\n            let sb = stringBuilderPool.Get()\n\n            try\n                let relativeDirectoryPath =\n                    relativePathParts\n                    |> Array.fold (fun (sb: StringBuilder) currentPart -> sb.Append($\"{currentPart}{Path.DirectorySeparatorChar}\")) sb\n\n                relativeDirectoryPath.Remove(relativeDirectoryPath.Length - 1, 1)\n                |> ignore\n                //logToConsole $\"In getRelativeDirectory: relativeDirectoryPath.ToString(): {relativeDirectoryPath.ToString()}\"\n                (relativeDirectoryPath.ToString())\n            finally\n                stringBuilderPool.Return(sb)\n\n\n    [<Literal>]\n    let CorrelationIdAlphabet = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._-\"\n\n    /// Returns a randomly-generated, 12-character NanoId as a new CorrelationId.\n    let generateCorrelationId () =\n        // According to https://alex7kom.github.io/nano-nanoid-cc/?alphabet=~._-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&size=12&speed=1000&speedUnit=second\n        //   if we generate 1000 NanoId's per second with our CorrelationIdAlphabet, it'll take 4 months before there's even a 1% chance of a collision.\n        //\n        //   (It'll be a while before Grace requires 1000 CorrelationId's per second, but let's assume it might.)\n        //\n        //   Even if there is a collision, who cares? it's just a CorrelationId, and it will be in requests for different owners/orgs/repos/branches/etc.\n        //\n        //   One of the really nice features of NanoId's vs. Guid's is that you get to choose the strength of the uniqueness guarantee by choosing the size of the NanoId.\n        //   I'm selecting 12 characters because it's just long enough to be reliably unique enough for _this_ purpose. CorrelationId's show up everywhere\n        //   in Grace, and get stored in multiple ways, so it's nice to keep them as small as possible while being unique _enough_.\n        //\n        //   The caller can always specify their own CorrelationId if they want to.\n        NanoidDotNet.Nanoid.Generate(CorrelationIdAlphabet, size = 12)\n\n    /// Returns either the supplied correlationId, if not null, or a new Guid.\n    let ensureNonEmptyCorrelationId (correlationId: string) =\n        if not <| String.IsNullOrEmpty(correlationId) then\n            correlationId\n        else\n            generateCorrelationId ()\n\n    /// Formats a byte array as a string. For example, [0xab, 0x15, 0x03] -> \"ab1503\"\n    ///\n    /// Each byte is formatted as a two-character hexadecimal number.\n    ///\n    /// NOTE: This is different from Encoding.UTF8.GetString, which interprets the bytes as UTF-8 characters.\n    let byteArrayToString (array: Span<byte>) =\n        let sb = stringBuilderPool.Get()\n\n        try\n            for b in array do\n                sb.Append($\"{b:x2}\") |> ignore\n\n            sb.ToString()\n        finally\n            stringBuilderPool.Return(sb)\n\n    /// Converts a string of hexadecimal numbers to a byte array. For example, \"ab1503\" -> [0xab, 0x15, 0x03]\n    ///\n    /// The hex string must have an even number of digits; for this function, \"1a8\" will throw an ArgumentException, but \"01a8\" is valid and will be converted to a byte array.\n    ///\n    /// NOTE: This is different from Encoding.UTF8.GetBytes(), which interprets the string as UTF-8 characters.\n    let stringAsByteArray (s: ReadOnlySpan<char>) =\n        if s.Length % 2 <> 0 then\n            raise (ArgumentException(\"The hexadecimal string must have an even number of digits.\", nameof s))\n\n        let byteArrayLength = int32 (s.Length / 2)\n        let bytes = Array.zeroCreate byteArrayLength\n\n        for index in [ 0..byteArrayLength ] do\n            let byteValue = s.Slice(index * 2, 2)\n            bytes[index] <- Byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture)\n\n        bytes\n\n    /// Serializes any value to JSON using Grace's default JsonSerializerOptions, and then converts the JSON string to a byte array.\n    let toByteArray<'T> (value: 'T) =\n        let json = serialize value\n        Encoding.UTF8.GetBytes json\n\n    /// Deserializes a byte array to a value of type 'T' using Grace's default JsonSerializerOptions.\n    let fromByteArray<'T> (bytes: ReadOnlySpan<byte>) =\n        let json = Encoding.UTF8.GetString(bytes)\n        deserialize<'T> json\n\n    /// The universal Grace exception response type\n    type ExceptionResponse =\n        {\n            ``exception``: string\n            innerException: string\n        }\n\n        override this.ToString() =\n            match this.innerException with\n            | null -> $\"Exception: {this.``exception``}{Environment.NewLine}{Environment.NewLine}\"\n            | innerEx -> $\"Exception: {this.``exception``}{Environment.NewLine}{Environment.NewLine}Inner exception: {this.innerException}{Environment.NewLine}\"\n\n        /// Creates an ExceptionResponse instance from an Exception-based instance.\n        static member Create(ex: Exception) =\n            //#if DEBUG\n            let exceptionMessage (ex: Exception) =\n                $\"Message: {ex.Message}{Environment.NewLine}{Environment.NewLine}Stack trace:{Environment.NewLine}{ex.StackTrace}{Environment.NewLine}\"\n\n            match ex.InnerException with\n            | null -> { ``exception`` = exceptionMessage ex; innerException = \"null\" }\n            | innerEx -> { ``exception`` = exceptionMessage ex; innerException = exceptionMessage ex.InnerException }\n    //#else\n    //        {|message = $\"Internal server error, and, yes, it's been logged. The correlationId is in the X-Correlation-Id header.\"|}\n    //#endif\n\n    let flattenValueTask (valueTask: ValueTask<ValueTask<'T>>) =\n        if valueTask.IsCompleted then\n            valueTask.Result\n        else\n            valueTask.GetAwaiter().GetResult()\n\n    /// Calls Task.FromResult<'T>() with the provided value.\n    let returnTask<'T> value = Task.FromResult<'T>(value)\n\n    /// Calls ValueTask.FromResult<'T>() with the provided value.\n    let returnValueTask<'T> value = ValueTask.FromResult<'T>(value)\n\n    /// Monadic bind for the nested monad Task<Result<'T, 'TError>>.\n    let bindTaskResult (result: Task<Result<'T, 'TError>>) (f: 'T -> Task<Result<'U, 'TError>>) =\n        (task {\n            match! result with\n            | Ok returnValue -> return (f returnValue)\n            | Error error -> return Error error |> returnTask\n        })\n            .Unwrap()\n\n    /// Custom monadic bind operator for the nested monad Task<Result<'T, 'TError>>.\n    let inline (>>=!) (result: Task<Result<'T, 'TError>>) (f: 'T -> Task<Result<'U, 'TError>>) = bindTaskResult result f\n\n    //let inline (>>=!) (result: ValueTask<Result<'T, 'TError>>) (f: 'T -> ValueTask<Result<'U, 'TError>>) =\n    //    bindTaskResult result f\n\n    /// Checks if a string begins with a path separator character.\n    let pathContainsSeparator (path: string) =\n        path.Contains(Path.DirectorySeparatorChar)\n        || path.Contains(Path.AltDirectorySeparatorChar)\n\n    /// Returns the number of segments in a given path.\n    ///\n    /// Examples:\n    ///\n    /// \"foo/bar/demo.js\" -> 3\n    ///\n    /// \"topLevelFile.js\" -> 1\n    ///\n    /// \".\" (i.e. root directory) -> 0\n    let countSegments (path: string) =\n        if path.Contains(Path.DirectorySeparatorChar) then\n            path.Split(Path.DirectorySeparatorChar).Length\n        elif path.Contains(Path.AltDirectorySeparatorChar) then\n            path.Split(Path.AltDirectorySeparatorChar).Length\n        elif path = Constants.RootDirectoryPath then\n            0\n        else\n            1\n\n    /// Returns the parent directory path of a given path, or None if this is the root directory of the repository.\n    let getParentPath (path: string) =\n        if path = Constants.RootDirectoryPath then\n            None\n        else\n            let lastIndex =\n                path.LastIndexOfAny(\n                    [|\n                        Path.DirectorySeparatorChar\n                        Path.AltDirectorySeparatorChar\n                    |]\n                )\n\n            if lastIndex = -1 then\n                Some Constants.RootDirectoryPath\n            else\n                Some(path.Substring(0, lastIndex))\n\n    /// Gets a value for the Content-Type HTTP header for storing a file.\n    ///\n    /// If the file extension is found in the MimeTypes package, we'll use the content type from there.\n    /// If it's not, and the file is binary, we'll use \"application/octet-stream\", otherwise we'll use \"application/text\".\n    let getContentType (fileInfo: FileInfo) isBinary =\n        let mutable mimeType = String.Empty\n\n        if MimeTypes.MimeTypeMap.TryGetMimeType(fileInfo.Name, &mimeType) then mimeType\n        elif isBinary then \"application/octet-stream\"\n        else \"application/text\"\n\n    /// Creates a Span<`T> on the stack to minimize heap usage and GC. This is an F# implementation of the C# keyword `stackalloc`.\n    /// This should be used for smaller allocations, as the stack has ~1MB size.\n    // Borrowed with appreciation from https://bartoszsypytkowski.com/writing-high-performance-f-code/.\n    let inline stackalloc<'a when 'a: unmanaged> (length: int) : Span<'a> =\n        let p =\n            NativePtr.stackalloc<'a> length\n            |> NativePtr.toVoidPtr\n\n        Span<'a>(p, length)\n\n    let propertyLookupByType = ConcurrentDictionary<Type, PropertyInfo array>()\n\n    /// Creates a dictionary from the property names and values of a set of parameters.\n    let getParametersAsDictionary<'T> (obj: 'T) =\n        let mutable properties = Array.Empty<PropertyInfo>()\n\n        if\n            not\n            <| propertyLookupByType.TryGetValue(typeof<'T>, &properties)\n        then\n            properties <- typeof<'T>.GetProperties (BindingFlags.Instance ||| BindingFlags.Public)\n\n            propertyLookupByType.TryAdd(typeof<'T>, properties)\n            |> ignore\n\n        let dictionary = Dictionary<string, obj>()\n\n        for prop in properties do\n            let value = prop.GetValue(obj)\n\n            dictionary[$\"parameter:{prop.Name}\"] <- value\n\n        dictionary :> IReadOnlyDictionary<string, obj>\n\n    /// This construct is equivalent to using IHttpClientFactory in the ASP.NET Dependency Injection container, for code (like this) that isn't using GenericHost.\n    ///\n    /// See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-8.0#alternatives-to-ihttpclientfactory for more information.\n    let socketsHttpHandler =\n        new SocketsHttpHandler(\n            AllowAutoRedirect = true, // We expect to use Traffic Manager or equivalents, so there will be redirects.\n            MaxAutomaticRedirections = 6, // Not sure of the exact right number, but definitely want a limit here.\n            SslOptions =\n                SslClientAuthenticationOptions(\n                    EnabledSslProtocols =\n                        Security.Authentication.SslProtocols.Tls12\n                        + Security.Authentication.SslProtocols.Tls13\n                ),\n            AutomaticDecompression = DecompressionMethods.All, // We'll store blobs using GZip, and we'll enable Brotli on the server\n            EnableMultipleHttp2Connections = true, // I doubt this will ever happen, but don't mind making it possible\n            PooledConnectionLifetime = TimeSpan.FromMinutes(2.0), // Default is 2m\n            PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2.0) // Default is 2m\n        )\n\n    /// Gets an HttpClient instance from an enhanced, custom HttpClientFactory.\n    let getHttpClient (correlationId: string) =\n        let traceIdBytes = stackalloc<byte> 16\n        let parentIdBytes = stackalloc<byte> 8\n        Random.Shared.NextBytes(traceIdBytes)\n        Random.Shared.NextBytes(parentIdBytes)\n        let traceId = byteArrayToString (traceIdBytes)\n        let parentId = byteArrayToString (parentIdBytes)\n\n        let httpClient = new HttpClient(handler = socketsHttpHandler, disposeHandler = false)\n\n        httpClient.DefaultRequestVersion <- HttpVersion.Version20 // We'll aggressively move to Version30 as soon as we can.\n        httpClient.DefaultRequestHeaders.Add(Constants.Traceparent, $\"00-{traceId}-{parentId}-01\")\n        httpClient.DefaultRequestHeaders.Add(Constants.Tracestate, $\"graceserver-{parentId}\")\n        httpClient.DefaultRequestHeaders.Add(Constants.CorrelationIdHeaderKey, $\"{correlationId}\")\n        httpClient.DefaultRequestHeaders.Add(Constants.ServerApiVersionHeaderKey, \"Edge\")\n        //httpClient.DefaultVersionPolicy <- HttpVersionPolicy.RequestVersionOrHigher\n#if DEBUG\n        httpClient.Timeout <- TimeSpan.FromSeconds(1800.0) // Keeps client commands open while debugging.\n        //httpClient.Timeout <- TimeSpan.FromSeconds(7.5)  // Fast fail for normal testing.\n#else\n        httpClient.Timeout <- TimeSpan.FromSeconds(15.0) // Fast fail for testing network connectivity.\n#endif\n        httpClient\n\n    /// Returns the object file name for a given relative path, including the SHA-256 hash.\n    /// Example: foo.txt with a SHA-256 hash of \"8e798...980c\" -> \"foo_8e798...980c.txt\".\n    let getObjectFileName (relativePath: string) (sha256Hash: string) =\n        let file = FileInfo(relativePath)\n\n        if file.Extension = String.Empty then\n            $\"{file.Name}_{sha256Hash}\"\n        else\n            $\"{file.Name.Replace(file.Extension, String.Empty)}_{sha256Hash}{file.Extension}\"\n\n    /// Gets the name of the machine or node, without the prefix of the container name.\n    let getMachineName =\n        let nameParts = Environment.MachineName.Split('-')\n        // return the last two parts of the machine name with a hyphen in between\n        if nameParts.Length > 1 then\n            $\"{nameParts.[nameParts.Length - 2]}-{nameParts.[nameParts.Length - 1]}\"\n        else\n            Environment.MachineName\n"
  },
  {
    "path": "src/Grace.Shared/Validation/Common.Validation.fs",
    "content": "namespace Grace.Shared.Validation\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen System\nopen System.Collections.Generic\nopen System.Text.RegularExpressions\nopen System.Threading.Tasks\n\nmodule Common =\n\n    module Guid =\n\n        /// Validates that a string is a valid Guid, and is not Guid.Empty.\n        let isValidAndNotEmptyGuid<'T when 'T :> IErrorDiscriminatedUnion> (s: string) (error: 'T) =\n            if not <| String.IsNullOrEmpty(s) then\n                let mutable guid = Guid.Empty\n\n                if Guid.TryParse(s, &guid) && guid <> Guid.Empty then\n                    Ok() |> returnValueTask\n                else\n                    Error error |> returnValueTask\n            else\n                Ok() |> returnValueTask\n\n        /// Validates that a guid is not Guid.Empty.\n        let isNotEmpty<'T when 'T :> IErrorDiscriminatedUnion> (guid: Guid) (error: 'T) =\n            if guid = Guid.Empty then\n                Error error |> returnValueTask\n            else\n                Ok() |> returnValueTask\n\n    module Number =\n\n        /// Validates that the given number is positive.\n        let isPositiveOrZero<'T when 'T :> IErrorDiscriminatedUnion> (n: double) (error: 'T) =\n            if n >= 0.0 then Ok() |> returnValueTask else Error error |> returnValueTask\n\n        /// Validates that a number is found between the supplied lower and upper bounds.\n        let isWithinRange<'T, 'U when 'T: comparison and 'U :> IErrorDiscriminatedUnion> (n: 'T) (lower: 'T) (upper: 'T) (error: 'U) =\n            if lower <= n && n <= upper then\n                Ok() |> returnValueTask\n            else\n                Error error |> returnValueTask\n\n    module String =\n\n        /// Checks that the provided string is a valid Grace name (i.e. it matches GraceNameRegex).\n        let isValidGraceName<'T when 'T :> IErrorDiscriminatedUnion> (name: string) (error: 'T) =\n            if\n                String.IsNullOrEmpty(name)\n                || Constants.GraceNameRegex.IsMatch(name)\n            then\n                Ok() |> returnValueTask\n            else\n                Error error |> returnValueTask\n\n        /// Validates that a string is not empty or null.\n        let isNotEmpty<'T when 'T :> IErrorDiscriminatedUnion> (s: string) (error: 'T) =\n            if String.IsNullOrEmpty(s) then\n                Error error |> returnValueTask\n            else\n                Ok() |> returnValueTask\n\n        /// Validates that a string is either empty or a valid partial or full SHA-256 hash value.\n        ///\n        /// Regex: ^[0-9a-fA-F]{2,64}$\n        let isEmptyOrValidSha256Hash<'T when 'T :> IErrorDiscriminatedUnion> (s: string) (error: 'T) =\n            if\n                String.IsNullOrEmpty(s)\n                || Constants.Sha256Regex.IsMatch(s)\n            then\n                Ok() |> returnValueTask\n            else\n                Error error |> returnValueTask\n\n        /// Validates that a string is a valid partial or full SHA-256 hash value.\n        ///\n        /// Regex: ^[0-9a-fA-F]{2,64}$\n        let isValidSha256Hash<'T when 'T :> IErrorDiscriminatedUnion> (s: string) (error: 'T) =\n            if Constants.Sha256Regex.IsMatch(s) then\n                Ok() |> returnValueTask\n            else\n                Error error |> returnValueTask\n\n        /// Validates that a string is no longer than the specified length.\n        let maxLength<'T when 'T :> IErrorDiscriminatedUnion> (s: string) (maxLength: int) (error: 'T) =\n            if s.Length > maxLength then\n                Error error |> returnValueTask\n            else\n                Ok() |> returnValueTask\n\n    module DiscriminatedUnion =\n\n        /// Validates that a string is a member of the supplied discriminated union type.\n        let isMemberOf<'T, 'U when 'U :> IErrorDiscriminatedUnion> (s: string) (error: 'U) =\n            match Utilities.discriminatedUnionFromString<'T> (s) with\n            | Some _ -> Ok() |> returnValueTask\n            | None -> Error error |> returnValueTask\n\n    module Input =\n\n        /// Validates that we have a value for either the supplied id, or the supplied name.\n        let eitherIdOrNameMustBeProvided<'T when 'T :> IErrorDiscriminatedUnion> id name (error: 'T) =\n            if\n                String.IsNullOrEmpty(id)\n                && String.IsNullOrEmpty(name)\n            then\n                Error error |> returnValueTask\n            else\n                Ok() |> returnValueTask\n\n        /// Validates that a list is non-empty.\n        let listIsNonEmpty<'T, 'U when 'U :> IErrorDiscriminatedUnion> (list: IEnumerable<'T>) (error: 'U) =\n            let xs = List<'T>(list)\n\n            if xs.Count > 0 then Ok() |> returnValueTask else Error error |> returnValueTask\n\n        /// Validates that one of the values passed in the array is not null, if it's a string, it's not empty, and if it's a Guid, it's not Guid.Empty.\n        let oneOfTheseValuesMustBeProvided<'T when 'T :> IErrorDiscriminatedUnion> (values: Object array) (error: 'T) =\n            match values\n                  |> Array.tryFind (fun value ->\n                      match value with\n                      | null -> false\n                      | :? string as s -> not <| String.IsNullOrEmpty(s)\n                      | :? Guid as g -> g <> Guid.Empty\n                      | _ -> true)\n                with\n            | Some _ -> Ok() |> returnValueTask\n            | None -> Error error |> returnValueTask\n"
  },
  {
    "path": "src/Grace.Shared/Validation/Connect.Validation.fs",
    "content": "namespace Grace.Shared.Validation\n\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Validation.Errors\nopen System\n\nmodule Connect =\n\n    let saveDaysIsAPositiveNumber (saveDays: double) (error: ConnectError) = if saveDays < 0.0 then Error error else Ok()\n\n    let visibilityIsValid (visibility: string) (error: ConnectError) =\n        match Utilities.discriminatedUnionFromString<RepositoryType> (visibility) with\n        | Some visibility -> Ok()\n        | None -> Error error\n"
  },
  {
    "path": "src/Grace.Shared/Validation/Errors.Validation.fs",
    "content": "namespace Grace.Shared.Validation\n\nopen Grace.Shared.Resources.Text\nopen Grace.Shared.Resources.Utilities\nopen System\n\nmodule Errors =\n\n    // Marker interface + compile-time constraint\n    type IErrorDiscriminatedUnion =\n        interface\n        end\n\n    type BranchError =\n        | AssignIsDisabled\n        | BranchAlreadyExists\n        | BranchDoesNotExist\n        | BranchIdDoesNotExist\n        | BranchIdIsRequired\n        | BranchIsNotBasedOnLatestPromotion\n        | BranchNameAlreadyExists\n        | BranchNameIsRequired\n        | CannotDeleteBranchesWithChildrenWithoutReassigningChildren\n        | CannotSpecifyBothForceAndReassignChildBranches\n        | CheckpointIsDisabled\n        | CommitIsDisabled\n        | DuplicateCorrelationId\n        | EitherBranchIdOrBranchNameRequired\n        | EitherDirectoryVersionIdOrSha256HashRequired\n        | EitherOrganizationIdOrOrganizationNameRequired\n        | EitherOwnerIdOrOwnerNameRequired\n        | EitherRepositoryIdOrRepositoryNameIsRequired\n        | EitherToBranchIdOrToBranchNameIsRequired\n        | ExternalIsDisabled\n        | FailedToAddReference\n        | FailedToRetrieveBranch\n        | FailedWhileApplyingEvent\n        | IndexFileNotFound\n        | InvalidBranchId\n        | InvalidBranchName\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidReferenceId\n        | InvalidReferenceType\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidSha256Hash\n        | PromotionIsDisabled\n        | PromotionNotAvailableBecauseThereAreNoPromotableReferences\n        | MessageIsRequired\n        | ObjectCacheFileNotFound\n        | OrganizationDoesNotExist\n        | OwnerDoesNotExist\n        | ParentBranchDoesNotExist\n        | ParentBranchDoesNotAllowPromotions\n        | ReferenceIdDoesNotExist\n        | ReferenceTypeMustBeProvided\n        | RepositoryDoesNotExist\n        | SaveIsDisabled\n        | Sha256HashDoesNotExist\n        | Sha256HashIsRequired\n        | StringIsTooLong\n        | TagIsDisabled\n        | ValueMustBePositive\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(branchError: BranchError) : string =\n            match branchError with\n            | AssignIsDisabled -> getLocalizedString StringResourceName.AssignIsDisabled\n            | BranchAlreadyExists -> getLocalizedString StringResourceName.BranchAlreadyExists\n            | BranchDoesNotExist -> getLocalizedString StringResourceName.BranchDoesNotExist\n            | BranchIdDoesNotExist -> getLocalizedString StringResourceName.BranchIdDoesNotExist\n            | BranchIdIsRequired -> getLocalizedString StringResourceName.BranchIdIsRequired\n            | BranchIsNotBasedOnLatestPromotion -> getLocalizedString StringResourceName.BranchIsNotBasedOnLatestPromotion\n            | BranchNameAlreadyExists -> getLocalizedString StringResourceName.BranchNameAlreadyExists\n            | BranchNameIsRequired -> getLocalizedString StringResourceName.BranchNameIsRequired\n            | CannotDeleteBranchesWithChildrenWithoutReassigningChildren ->\n                \"You cannot delete a branch with children. Use --reassign-child-branches to reassign them to another parent, or --force to delete all child branches.\"\n            | CannotSpecifyBothForceAndReassignChildBranches ->\n                \"You cannot specify both --force and --reassign-child-branches. Use --force to delete child branches, or --reassign-child-branches to move them to a new parent.\"\n            | CheckpointIsDisabled -> getLocalizedString StringResourceName.CheckpointIsDisabled\n            | CommitIsDisabled -> getLocalizedString StringResourceName.CommitIsDisabled\n            | DuplicateCorrelationId -> getLocalizedString StringResourceName.DuplicateCorrelationId\n            | EitherBranchIdOrBranchNameRequired -> getLocalizedString StringResourceName.EitherBranchIdOrBranchNameIsRequired\n            | EitherDirectoryVersionIdOrSha256HashRequired -> getLocalizedString StringResourceName.EitherDirectoryVersionIdOrSha256HashRequired\n            | EitherOrganizationIdOrOrganizationNameRequired -> getLocalizedString StringResourceName.EitherOrganizationIdOrOrganizationNameIsRequired\n            | EitherOwnerIdOrOwnerNameRequired -> getLocalizedString StringResourceName.EitherOwnerIdOrOwnerNameIsRequired\n            | EitherRepositoryIdOrRepositoryNameIsRequired -> getLocalizedString StringResourceName.EitherRepositoryIdOrRepositoryNameIsRequired\n            | EitherToBranchIdOrToBranchNameIsRequired -> getLocalizedString StringResourceName.EitherToBranchIdOrToBranchNameIsRequired\n            | ExternalIsDisabled -> getLocalizedString StringResourceName.ExternalIsDisabled\n            | FailedToAddReference -> getLocalizedString StringResourceName.FailedToAddReference\n            | FailedToRetrieveBranch -> getLocalizedString StringResourceName.FailedToRetrieveBranch\n            | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent\n            | IndexFileNotFound -> getLocalizedString StringResourceName.IndexFileNotFound\n            | InvalidBranchId -> getLocalizedString StringResourceName.InvalidBranchId\n            | InvalidBranchName -> getLocalizedString StringResourceName.InvalidBranchName\n            | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId\n            | InvalidOrganizationName -> getLocalizedString StringResourceName.InvalidOrganizationName\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidReferenceId -> getLocalizedString StringResourceName.InvalidReferenceId\n            | InvalidReferenceType -> getLocalizedString StringResourceName.InvalidReferenceType\n            | InvalidRepositoryId -> getLocalizedString StringResourceName.InvalidRepositoryId\n            | InvalidRepositoryName -> getLocalizedString StringResourceName.InvalidRepositoryName\n            | InvalidSha256Hash -> getLocalizedString StringResourceName.InvalidSha256Hash\n            | PromotionIsDisabled -> getLocalizedString StringResourceName.PromotionIsDisabled\n            | PromotionNotAvailableBecauseThereAreNoPromotableReferences ->\n                getLocalizedString StringResourceName.PromotionNotAvailableBecauseThereAreNoPromotableReferences\n            | MessageIsRequired -> getLocalizedString StringResourceName.MessageIsRequired\n            | ObjectCacheFileNotFound -> getLocalizedString StringResourceName.ObjectCacheFileNotFound\n            | OrganizationDoesNotExist -> getLocalizedString StringResourceName.OrganizationDoesNotExist\n            | OwnerDoesNotExist -> getLocalizedString StringResourceName.OwnerDoesNotExist\n            | ParentBranchDoesNotExist -> getLocalizedString StringResourceName.ParentBranchDoesNotExist\n            | ParentBranchDoesNotAllowPromotions -> getLocalizedString StringResourceName.ParentBranchDoesNotAllowPromotions\n            | ReferenceIdDoesNotExist -> getLocalizedString StringResourceName.ReferenceIdDoesNotExist\n            | ReferenceTypeMustBeProvided -> getLocalizedString StringResourceName.ReferenceTypeMustBeProvided\n            | RepositoryDoesNotExist -> getLocalizedString StringResourceName.RepositoryDoesNotExist\n            | SaveIsDisabled -> getLocalizedString StringResourceName.SaveIsDisabled\n            | Sha256HashDoesNotExist -> getLocalizedString StringResourceName.Sha256HashDoesNotExist\n            | Sha256HashIsRequired -> getLocalizedString StringResourceName.Sha256HashIsRequired\n            | StringIsTooLong -> getLocalizedString StringResourceName.StringIsTooLong\n            | TagIsDisabled -> getLocalizedString StringResourceName.TagIsDisabled\n            | ValueMustBePositive -> getLocalizedString StringResourceName.ValueMustBePositive\n\n        static member getErrorMessage(branchError: BranchError option) : string =\n            match branchError with\n            | Some error -> BranchError.getErrorMessage error\n            | None -> String.Empty\n\n    type ConfigError =\n        | InvalidDirectoryPath\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(configError: ConfigError) : string =\n            match configError with\n            | InvalidDirectoryPath -> getLocalizedString StringResourceName.InvalidDirectoryPath\n\n        static member getErrorMessage(configError: ConfigError option) : string =\n            match configError with\n            | Some error -> ConfigError.getErrorMessage error\n            | None -> String.Empty\n\n    type ConnectError =\n        | RepositoryDoesNotExist\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n\n    type DiffError =\n        | DirectoryDoesNotExist\n        | InvalidDirectoryVersionId\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidSha256Hash\n        | Sha256HashIsRequired\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(diffError: DiffError) : string =\n            match diffError with\n            | DirectoryDoesNotExist -> getLocalizedString StringResourceName.DirectoryDoesNotExist\n            | InvalidDirectoryVersionId -> getLocalizedString StringResourceName.InvalidDirectoryVersionId\n            | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId\n            | InvalidOrganizationName -> getLocalizedString StringResourceName.InvalidOrganizationName\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidRepositoryId -> getLocalizedString StringResourceName.InvalidRepositoryId\n            | InvalidRepositoryName -> getLocalizedString StringResourceName.InvalidRepositoryName\n            | InvalidSha256Hash -> getLocalizedString StringResourceName.InvalidSha256Hash\n            | Sha256HashIsRequired -> getLocalizedString StringResourceName.Sha256HashIsRequired\n\n        static member getErrorMessage(diffError: DiffError option) : string =\n            match diffError with\n            | Some error -> DiffError.getErrorMessage error\n            | None -> String.Empty\n\n    type DirectoryVersionError =\n        | DirectoryAlreadyExists\n        | DirectoryDoesNotExist\n        | DirectorySha256HashAlreadyExists\n        | FailedWhileApplyingEvent\n        | FileNotFoundInObjectStorage\n        | FileSha256HashDoesNotMatch\n        | IndexFileNotFound\n        | InvalidDirectoryVersionId\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidSha256Hash\n        | InvalidSize\n        | ObjectCacheFileNotFound\n        | RelativePathMustNotBeEmpty\n        | RepositoryDoesNotExist\n        | Sha256HashIsRequired\n        | Sha256HashDoesNotMatch\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(directoryError: DirectoryVersionError) : string =\n            match directoryError with\n            | DirectoryAlreadyExists -> getLocalizedString StringResourceName.DirectoryAlreadyExists\n            | DirectoryDoesNotExist -> getLocalizedString StringResourceName.DirectoryDoesNotExist\n            | DirectorySha256HashAlreadyExists -> getLocalizedString StringResourceName.DirectorySha256HashAlreadyExists\n            | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent\n            | FileNotFoundInObjectStorage -> getLocalizedString StringResourceName.FileNotFoundInObjectStorage\n            | FileSha256HashDoesNotMatch -> getLocalizedString StringResourceName.FileSha256HashDoesNotMatch\n            | IndexFileNotFound -> getLocalizedString StringResourceName.IndexFileNotFound\n            | InvalidDirectoryVersionId -> getLocalizedString StringResourceName.InvalidDirectoryVersionId\n            | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId\n            | InvalidOrganizationName -> getLocalizedString StringResourceName.InvalidOrganizationName\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidRepositoryId -> getLocalizedString StringResourceName.InvalidRepositoryId\n            | InvalidRepositoryName -> getLocalizedString StringResourceName.InvalidRepositoryName\n            | InvalidSha256Hash -> getLocalizedString StringResourceName.InvalidSha256Hash\n            | InvalidSize -> getLocalizedString StringResourceName.InvalidSize\n            | ObjectCacheFileNotFound -> getLocalizedString StringResourceName.ObjectCacheFileNotFound\n            | RelativePathMustNotBeEmpty -> getLocalizedString StringResourceName.RelativePathMustNotBeEmpty\n            | RepositoryDoesNotExist -> getLocalizedString StringResourceName.RepositoryDoesNotExist\n            | Sha256HashIsRequired -> getLocalizedString StringResourceName.Sha256HashIsRequired\n            | Sha256HashDoesNotMatch -> getLocalizedString StringResourceName.Sha256HashDoesNotMatch\n\n        static member getErrorMessage(directoryError: DirectoryVersionError option) : string =\n            match directoryError with\n            | Some error -> DirectoryVersionError.getErrorMessage error\n            | None -> String.Empty\n\n    type OwnerError =\n        | DeleteReasonIsRequired\n        | DescriptionIsRequired\n        | DescriptionIsTooLong\n        | DuplicateCorrelationId\n        | EitherOwnerIdOrOwnerNameRequired\n        | FailedWhileApplyingEvent\n        | FailedWhileSavingEvent\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidOwnerType\n        | InvalidSearchVisibility\n        | OwnerContainsOrganizations\n        | OwnerDoesNotExist\n        | OwnerIdAlreadyExists\n        | OwnerIdDoesNotExist\n        | OwnerIdIsRequired\n        | OwnerIsDeleted\n        | OwnerIsNotDeleted\n        | OwnerNameIsRequired\n        | OwnerNameAlreadyExists\n        | OwnerTypeIsRequired\n        | SearchVisibilityIsRequired\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(ownerError: OwnerError) : string =\n            match ownerError with\n            | DeleteReasonIsRequired -> getLocalizedString StringResourceName.DeleteReasonIsRequired\n            | DescriptionIsRequired -> getLocalizedString StringResourceName.DescriptionIsRequired\n            | DescriptionIsTooLong -> getLocalizedString StringResourceName.DescriptionIsTooLong\n            | DuplicateCorrelationId -> getLocalizedString StringResourceName.DuplicateCorrelationId\n            | EitherOwnerIdOrOwnerNameRequired -> getLocalizedString StringResourceName.EitherOwnerIdOrOwnerNameIsRequired\n            | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent\n            | FailedWhileSavingEvent -> getLocalizedString StringResourceName.FailedWhileSavingEvent\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidOwnerType -> getLocalizedString StringResourceName.InvalidOwnerType\n            | InvalidSearchVisibility -> getLocalizedString StringResourceName.InvalidSearchVisibility\n            | OwnerContainsOrganizations -> getLocalizedString StringResourceName.OwnerContainsOrganizations\n            | OwnerDoesNotExist -> getLocalizedString StringResourceName.OwnerDoesNotExist\n            | OwnerIdAlreadyExists -> getLocalizedString StringResourceName.OwnerIdAlreadyExists\n            | OwnerIdDoesNotExist -> getLocalizedString StringResourceName.OwnerIdDoesNotExist\n            | OwnerIdIsRequired -> getLocalizedString StringResourceName.OwnerIdIsRequired\n            | OwnerIsDeleted -> getLocalizedString StringResourceName.OwnerIsDeleted\n            | OwnerIsNotDeleted -> getLocalizedString StringResourceName.OwnerIsNotDeleted\n            | OwnerNameAlreadyExists -> getLocalizedString StringResourceName.OwnerNameAlreadyExists\n            | OwnerNameIsRequired -> getLocalizedString StringResourceName.OwnerNameIsRequired\n            | OwnerTypeIsRequired -> getLocalizedString StringResourceName.OwnerTypeIsRequired\n            | SearchVisibilityIsRequired -> getLocalizedString StringResourceName.SearchVisibilityIsRequired\n\n        static member getErrorMessage(ownerError: OwnerError option) : string =\n            match ownerError with\n            | Some error -> OwnerError.getErrorMessage error\n            | None -> String.Empty\n\n    type OrganizationError =\n        | DeleteReasonIsRequired\n        | DescriptionIsRequired\n        | DescriptionIsTooLong\n        | DuplicateCorrelationId\n        | EitherOrganizationIdOrOrganizationNameRequired\n        | EitherOwnerIdOrOwnerNameRequired\n        | ExceptionCaught\n        | FailedWhileApplyingEvent\n        | FailedWhileSavingEvent\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOrganizationType\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidSearchVisibility\n        | OrganizationIdAlreadyExists\n        | OrganizationNameAlreadyExists\n        | OrganizationContainsRepositories\n        | OrganizationDoesNotExist\n        | OrganizationIdDoesNotExist\n        | OrganizationIdIsRequired\n        | OrganizationIsDeleted\n        | OrganizationIsNotDeleted\n        | OrganizationNameIsRequired\n        | OrganizationTypeIsRequired\n        | OwnerDoesNotExist\n        | OwnerIdIsRequired\n        | RepositoryNameIsRequired\n        | SearchVisibilityIsRequired\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(organizationError: OrganizationError) : string =\n            match organizationError with\n            | DeleteReasonIsRequired -> getLocalizedString StringResourceName.DeleteReasonIsRequired\n            | DescriptionIsRequired -> getLocalizedString StringResourceName.DescriptionIsRequired\n            | DescriptionIsTooLong -> getLocalizedString StringResourceName.DescriptionIsTooLong\n            | DuplicateCorrelationId -> getLocalizedString StringResourceName.DuplicateCorrelationId\n            | EitherOrganizationIdOrOrganizationNameRequired -> getLocalizedString StringResourceName.EitherOrganizationIdOrOrganizationNameIsRequired\n            | EitherOwnerIdOrOwnerNameRequired -> getLocalizedString StringResourceName.EitherOwnerIdOrOwnerNameIsRequired\n            | ExceptionCaught -> getLocalizedString StringResourceName.ExceptionCaught\n            | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent\n            | FailedWhileSavingEvent -> getLocalizedString StringResourceName.FailedWhileSavingEvent\n            | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId\n            | InvalidOrganizationName -> getLocalizedString StringResourceName.InvalidOrganizationName\n            | InvalidOrganizationType -> getLocalizedString StringResourceName.InvalidOrganizationType\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidRepositoryId -> getLocalizedString StringResourceName.InvalidRepositoryId\n            | InvalidRepositoryName -> getLocalizedString StringResourceName.InvalidRepositoryName\n            | InvalidSearchVisibility -> getLocalizedString StringResourceName.InvalidSearchVisibility\n            | OrganizationIdAlreadyExists -> getLocalizedString StringResourceName.OrganizationIdAlreadyExists\n            | OrganizationNameAlreadyExists -> getLocalizedString StringResourceName.OrganizationNameAlreadyExists\n            | OrganizationContainsRepositories -> getLocalizedString StringResourceName.OrganizationContainsRepositories\n            | OrganizationDoesNotExist -> getLocalizedString StringResourceName.OrganizationDoesNotExist\n            | OrganizationIdIsRequired -> getLocalizedString StringResourceName.OrganizationIdIsRequired\n            | OrganizationIdDoesNotExist -> getLocalizedString StringResourceName.OrganizationIdDoesNotExist\n            | OrganizationIsDeleted -> getLocalizedString StringResourceName.OrganizationIsDeleted\n            | OrganizationIsNotDeleted -> getLocalizedString StringResourceName.OrganizationIsNotDeleted\n            | OrganizationNameIsRequired -> getLocalizedString StringResourceName.OrganizationNameIsRequired\n            | OrganizationTypeIsRequired -> getLocalizedString StringResourceName.OrganizationTypeIsRequired\n            | OwnerDoesNotExist -> getLocalizedString StringResourceName.OwnerDoesNotExist\n            | OwnerIdIsRequired -> getLocalizedString StringResourceName.OwnerIdIsRequired\n            | RepositoryNameIsRequired -> getLocalizedString StringResourceName.RepositoryNameIsRequired\n            | SearchVisibilityIsRequired -> getLocalizedString StringResourceName.SearchVisibilityIsRequired\n\n        static member getErrorMessage(organizationError: OrganizationError option) : string =\n            match organizationError with\n            | Some error -> OrganizationError.getErrorMessage error\n            | None -> String.Empty\n\n    type ReferenceError =\n        | AssignIsDisabled\n        | BranchDoesNotExist\n        | BranchIdDoesNotExist\n        | CheckpointIsDisabled\n        | CommitIsDisabled\n        | DuplicateCorrelationId\n        | EitherBranchIdOrBranchNameRequired\n        | EitherDirectoryVersionIdOrSha256HashRequired\n        | EitherOrganizationIdOrOrganizationNameRequired\n        | EitherOwnerIdOrOwnerNameRequired\n        | EitherRepositoryIdOrRepositoryNameIsRequired\n        | EitherToBranchIdOrToBranchNameIsRequired\n        | ExternalIsDisabled\n        | FailedToRetrieveBranch\n        | FailedWhileApplyingEvent\n        | IndexFileNotFound\n        | InvalidBranchId\n        | InvalidBranchName\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidReferenceId\n        | InvalidReferenceType\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidSha256Hash\n        | PromotionIsDisabled\n        | PromotionNotAvailableBecauseThereAreNoPromotableReferences\n        | MessageIsRequired\n        | ObjectCacheFileNotFound\n        | OrganizationDoesNotExist\n        | OwnerDoesNotExist\n        | ParentBranchDoesNotExist\n        | ReferenceAlreadyExists\n        | ReferenceIdDoesNotExist\n        | ReferenceTypeMustBeProvided\n        | RepositoryDoesNotExist\n        | SaveIsDisabled\n        | Sha256HashDoesNotExist\n        | Sha256HashIsRequired\n        | StringIsTooLong\n        | TagIsDisabled\n        | ValueMustBePositive\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(referenceError: ReferenceError) : string =\n            match referenceError with\n            | AssignIsDisabled -> getLocalizedString StringResourceName.AssignIsDisabled\n            | BranchDoesNotExist -> getLocalizedString StringResourceName.BranchDoesNotExist\n            | BranchIdDoesNotExist -> getLocalizedString StringResourceName.BranchIdDoesNotExist\n            | CheckpointIsDisabled -> getLocalizedString StringResourceName.CheckpointIsDisabled\n            | CommitIsDisabled -> getLocalizedString StringResourceName.CommitIsDisabled\n            | DuplicateCorrelationId -> getLocalizedString StringResourceName.DuplicateCorrelationId\n            | EitherBranchIdOrBranchNameRequired -> getLocalizedString StringResourceName.EitherBranchIdOrBranchNameIsRequired\n            | EitherDirectoryVersionIdOrSha256HashRequired -> getLocalizedString StringResourceName.EitherDirectoryVersionIdOrSha256HashRequired\n            | EitherOrganizationIdOrOrganizationNameRequired -> getLocalizedString StringResourceName.EitherOrganizationIdOrOrganizationNameIsRequired\n            | EitherOwnerIdOrOwnerNameRequired -> getLocalizedString StringResourceName.EitherOwnerIdOrOwnerNameIsRequired\n            | EitherRepositoryIdOrRepositoryNameIsRequired -> getLocalizedString StringResourceName.EitherRepositoryIdOrRepositoryNameIsRequired\n            | EitherToBranchIdOrToBranchNameIsRequired -> getLocalizedString StringResourceName.EitherToBranchIdOrToBranchNameIsRequired\n            | ExternalIsDisabled -> getLocalizedString StringResourceName.ExternalIsDisabled\n            | FailedToRetrieveBranch -> getLocalizedString StringResourceName.FailedToRetrieveBranch\n            | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent\n            | IndexFileNotFound -> getLocalizedString StringResourceName.IndexFileNotFound\n            | InvalidBranchId -> getLocalizedString StringResourceName.InvalidBranchId\n            | InvalidBranchName -> getLocalizedString StringResourceName.InvalidBranchName\n            | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId\n            | InvalidOrganizationName -> getLocalizedString StringResourceName.InvalidOrganizationName\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidReferenceId -> getLocalizedString StringResourceName.InvalidReferenceId\n            | InvalidReferenceType -> getLocalizedString StringResourceName.InvalidReferenceType\n            | InvalidRepositoryId -> getLocalizedString StringResourceName.InvalidRepositoryId\n            | InvalidRepositoryName -> getLocalizedString StringResourceName.InvalidRepositoryName\n            | InvalidSha256Hash -> getLocalizedString StringResourceName.InvalidSha256Hash\n            | PromotionIsDisabled -> getLocalizedString StringResourceName.PromotionIsDisabled\n            | PromotionNotAvailableBecauseThereAreNoPromotableReferences ->\n                getLocalizedString StringResourceName.PromotionNotAvailableBecauseThereAreNoPromotableReferences\n            | MessageIsRequired -> getLocalizedString StringResourceName.MessageIsRequired\n            | ObjectCacheFileNotFound -> getLocalizedString StringResourceName.ObjectCacheFileNotFound\n            | OrganizationDoesNotExist -> getLocalizedString StringResourceName.OrganizationDoesNotExist\n            | OwnerDoesNotExist -> getLocalizedString StringResourceName.OwnerDoesNotExist\n            | ParentBranchDoesNotExist -> getLocalizedString StringResourceName.ParentBranchDoesNotExist\n            | ReferenceAlreadyExists -> getLocalizedString StringResourceName.ReferenceAlreadyExists\n            | ReferenceIdDoesNotExist -> getLocalizedString StringResourceName.ReferenceIdDoesNotExist\n            | ReferenceTypeMustBeProvided -> getLocalizedString StringResourceName.ReferenceTypeMustBeProvided\n            | RepositoryDoesNotExist -> getLocalizedString StringResourceName.RepositoryDoesNotExist\n            | SaveIsDisabled -> getLocalizedString StringResourceName.SaveIsDisabled\n            | Sha256HashDoesNotExist -> getLocalizedString StringResourceName.Sha256HashDoesNotExist\n            | Sha256HashIsRequired -> getLocalizedString StringResourceName.Sha256HashIsRequired\n            | StringIsTooLong -> getLocalizedString StringResourceName.StringIsTooLong\n            | TagIsDisabled -> getLocalizedString StringResourceName.TagIsDisabled\n            | ValueMustBePositive -> getLocalizedString StringResourceName.ValueMustBePositive\n\n        static member getErrorMessage(branchError: ReferenceError option) : string =\n            match branchError with\n            | Some error -> ReferenceError.getErrorMessage error\n            | None -> String.Empty\n\n    type RepositoryError =\n        | BranchIdsAreRequired\n        | DeleteReasonIsRequired\n        | DescriptionIsRequired\n        | DescriptionIsTooLong\n        | DuplicateCorrelationId\n        | EitherOrganizationIdOrOrganizationNameRequired\n        | EitherOwnerIdOrOwnerNameRequired\n        | EitherRepositoryIdOrRepositoryNameRequired\n        | FailedCreatingEmptyDirectoryVersion\n        | FailedCreatingInitialBranch\n        | FailedCreatingInitialPromotion\n        | FailedRebasingInitialBranch\n        | FailedWhileSavingEvent\n        | FailedWhileApplyingEvent\n        | InvalidCheckpointDaysValue\n        | InvalidConflictResolutionPolicy\n        | InvalidDiffCacheDaysValue\n        | InvalidDirectory\n        | InvalidDirectoryVersionCacheDaysValue\n        | InvalidLogicalDeleteDaysValue\n        | InvalidMaxCountValue\n        | InvalidNewName\n        | InvalidObjectStorageProvider\n        | InvalidOrganizationId\n        | InvalidOrganizationName\n        | InvalidOwnerId\n        | InvalidOwnerName\n        | InvalidRepositoryId\n        | InvalidRepositoryName\n        | InvalidRepositoryStatus\n        | InvalidSaveDaysValue\n        | InvalidServerApiVersion\n        | InvalidVisibilityValue\n        | OrganizationIdIsRequired\n        | OrganizationDoesNotExist\n        | OwnerDoesNotExist\n        | ReferenceIdsAreRequired\n        | RepositoryContainsBranches\n        | RepositoryDoesNotExist\n        | RepositoryNameIsRequired\n        | RepositoryIdDoesNotExist\n        | RepositoryIdAlreadyExists\n        | RepositoryIdIsRequired\n        | RepositoryIsAlreadyInitialized\n        | RepositoryIsDeleted\n        | RepositoryIsNotDeleted\n        | RepositoryIsNotEmpty\n        | RepositoryNameAlreadyExists\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(repositoryError: RepositoryError) : string =\n            match repositoryError with\n            | BranchIdsAreRequired -> getLocalizedString StringResourceName.BranchIdsAreRequired\n            | DescriptionIsRequired -> getLocalizedString StringResourceName.DescriptionIsRequired\n            | DescriptionIsTooLong -> getLocalizedString StringResourceName.DescriptionIsTooLong\n            | DeleteReasonIsRequired -> getLocalizedString StringResourceName.DeleteReasonIsRequired\n            | DuplicateCorrelationId -> getLocalizedString StringResourceName.DuplicateCorrelationId\n            | EitherOrganizationIdOrOrganizationNameRequired -> getLocalizedString StringResourceName.EitherOrganizationIdOrOrganizationNameIsRequired\n            | EitherOwnerIdOrOwnerNameRequired -> getLocalizedString StringResourceName.EitherOwnerIdOrOwnerNameIsRequired\n            | EitherRepositoryIdOrRepositoryNameRequired -> getLocalizedString StringResourceName.EitherRepositoryIdOrRepositoryNameIsRequired\n            | FailedCreatingEmptyDirectoryVersion -> getLocalizedString StringResourceName.FailedCreatingEmptyDirectoryVersion\n            | FailedCreatingInitialBranch -> getLocalizedString StringResourceName.FailedCreatingInitialBranch\n            | FailedCreatingInitialPromotion -> getLocalizedString StringResourceName.FailedCreatingInitialPromotion\n            | FailedRebasingInitialBranch -> getLocalizedString StringResourceName.FailedRebasingInitialBranch\n            | FailedWhileSavingEvent -> getLocalizedString StringResourceName.FailedWhileSavingEvent\n            | FailedWhileApplyingEvent -> getLocalizedString StringResourceName.FailedWhileApplyingEvent\n            | InvalidCheckpointDaysValue -> getLocalizedString StringResourceName.InvalidCheckpointDaysValue\n            | InvalidConflictResolutionPolicy -> getLocalizedString StringResourceName.InvalidConflictResolutionPolicy\n            | InvalidDiffCacheDaysValue -> getLocalizedString StringResourceName.InvalidDiffCacheDaysValue\n            | InvalidDirectory -> getLocalizedString StringResourceName.InvalidDirectoryPath\n            | InvalidDirectoryVersionCacheDaysValue -> getLocalizedString StringResourceName.InvalidDirectoryVersionCacheDaysValue\n            | InvalidLogicalDeleteDaysValue -> getLocalizedString StringResourceName.InvalidLogicalDeleteDaysValue\n            | InvalidMaxCountValue -> getLocalizedString StringResourceName.InvalidMaxCountValue\n            | InvalidNewName -> getLocalizedString StringResourceName.InvalidNewName\n            | InvalidObjectStorageProvider -> getLocalizedString StringResourceName.InvalidObjectStorageProvider\n            | InvalidOwnerId -> getLocalizedString StringResourceName.InvalidOwnerId\n            | InvalidOrganizationId -> getLocalizedString StringResourceName.InvalidOrganizationId\n            | InvalidOrganizationName -> getLocalizedString StringResourceName.InvalidOrganizationName\n            | InvalidOwnerName -> getLocalizedString StringResourceName.InvalidOwnerName\n            | InvalidRepositoryId -> getLocalizedString StringResourceName.InvalidRepositoryId\n            | InvalidRepositoryName -> getLocalizedString StringResourceName.InvalidRepositoryName\n            | InvalidRepositoryStatus -> getLocalizedString StringResourceName.InvalidRepositoryStatus\n            | InvalidSaveDaysValue -> getLocalizedString StringResourceName.InvalidSaveDaysValue\n            | InvalidServerApiVersion -> getLocalizedString StringResourceName.InvalidServerApiVersion\n            | InvalidVisibilityValue -> getLocalizedString StringResourceName.InvalidVisibilityValue\n            | OrganizationIdIsRequired -> getLocalizedString StringResourceName.OrganizationIdIsRequired\n            | OrganizationDoesNotExist -> getLocalizedString StringResourceName.OrganizationDoesNotExist\n            | OwnerDoesNotExist -> getLocalizedString StringResourceName.OwnerDoesNotExist\n            | ReferenceIdsAreRequired -> getLocalizedString StringResourceName.ReferenceIdsAreRequired\n            | RepositoryContainsBranches -> getLocalizedString StringResourceName.RepositoryContainsBranches\n            | RepositoryDoesNotExist -> getLocalizedString StringResourceName.RepositoryDoesNotExist\n            | RepositoryIdAlreadyExists -> getLocalizedString StringResourceName.RepositoryIdAlreadyExists\n            | RepositoryIdDoesNotExist -> getLocalizedString StringResourceName.RepositoryIdDoesNotExist\n            | RepositoryIdIsRequired -> getLocalizedString StringResourceName.RepositoryIdIsRequired\n            | RepositoryIsAlreadyInitialized -> getLocalizedString StringResourceName.RepositoryIsAlreadyInitialized\n            | RepositoryIsDeleted -> getLocalizedString StringResourceName.RepositoryIsDeleted\n            | RepositoryIsNotDeleted -> getLocalizedString StringResourceName.RepositoryIsNotDeleted\n            | RepositoryIsNotEmpty -> getLocalizedString StringResourceName.RepositoryIsNotEmpty\n            | RepositoryNameIsRequired -> getLocalizedString StringResourceName.RepositoryNameIsRequired\n            | RepositoryNameAlreadyExists -> getLocalizedString StringResourceName.RepositoryNameAlreadyExists\n\n\n        static member getErrorMessage(repositoryError: RepositoryError option) : string =\n            match repositoryError with\n            | Some error -> RepositoryError.getErrorMessage error\n            | None -> String.Empty\n\n    type StorageError =\n        | FailedCommunicatingWithObjectStorage\n        | FailedToGetUploadUrls\n        | FailedUploadingFilesToObjectStorage\n        | FilesMustNotBeEmpty\n        | NotImplemented\n        | ObjectStorageException\n        | UnknownObjectStorageProvider\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(storageError: StorageError) : string =\n            match storageError with\n            | FailedCommunicatingWithObjectStorage -> getLocalizedString StringResourceName.FailedCommunicatingWithObjectStorage\n            | FailedToGetUploadUrls -> getLocalizedString StringResourceName.FailedToGetUploadUrls\n            | FailedUploadingFilesToObjectStorage -> getLocalizedString StringResourceName.FailedUploadingFilesToObjectStorage\n            | FilesMustNotBeEmpty -> getLocalizedString StringResourceName.FilesMustNotBeEmpty\n            | NotImplemented -> getLocalizedString StringResourceName.NotImplemented\n            | ObjectStorageException -> getLocalizedString StringResourceName.ObjectStorageException\n            | UnknownObjectStorageProvider -> getLocalizedString StringResourceName.UnknownObjectStorageProvider\n\n        static member getErrorMessage(storageError: StorageError option) : string =\n            match storageError with\n            | Some error -> StorageError.getErrorMessage error\n            | None -> String.Empty\n\n    type WorkItemError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | WorkItemAlreadyExists\n        | WorkItemDoesNotExist\n        | InvalidWorkItemId\n        | InvalidWorkItemNumber\n        | InvalidReferenceId\n        | InvalidArtifactId\n        | InvalidArtifactType\n        | InvalidPromotionSetId\n        | InvalidStatus\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(workItemError: WorkItemError) : string =\n            match workItemError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the work item event.\"\n            | WorkItemAlreadyExists -> \"A work item with this ID already exists.\"\n            | WorkItemDoesNotExist -> \"The specified work item does not exist.\"\n            | InvalidWorkItemId -> \"The work item ID is invalid.\"\n            | InvalidWorkItemNumber -> \"The work item number is invalid. Use a positive integer.\"\n            | InvalidReferenceId -> \"The reference ID is invalid.\"\n            | InvalidArtifactId -> \"The artifact ID is invalid.\"\n            | InvalidArtifactType -> \"The artifact type is invalid.\"\n            | InvalidPromotionSetId -> \"The promotion set ID is invalid.\"\n            | InvalidStatus -> \"The work item status is invalid.\"\n\n        static member getErrorMessage(workItemError: WorkItemError option) : string =\n            match workItemError with\n            | Some error -> WorkItemError.getErrorMessage error\n            | None -> String.Empty\n\n    type PolicyError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | PolicySnapshotAlreadyExists\n        | PolicySnapshotDoesNotExist\n        | InvalidTargetBranchId\n        | InvalidPolicySnapshotId\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(policyError: PolicyError) : string =\n            match policyError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the policy event.\"\n            | PolicySnapshotAlreadyExists -> \"The policy snapshot already exists.\"\n            | PolicySnapshotDoesNotExist -> \"The policy snapshot does not exist.\"\n            | InvalidTargetBranchId -> \"The target branch ID is invalid.\"\n            | InvalidPolicySnapshotId -> \"The policy snapshot ID is invalid.\"\n\n        static member getErrorMessage(policyError: PolicyError option) : string =\n            match policyError with\n            | Some error -> PolicyError.getErrorMessage error\n            | None -> String.Empty\n\n    type ReviewError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | ReviewNotesDoesNotExist\n        | FindingDoesNotExist\n        | InvalidPromotionSetId\n        | InvalidFindingId\n        | InvalidReferenceId\n        | InvalidPolicySnapshotId\n        | InvalidResolutionState\n        | InvalidChapterId\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(reviewError: ReviewError) : string =\n            match reviewError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the review event.\"\n            | ReviewNotesDoesNotExist -> \"The review notes do not exist.\"\n            | FindingDoesNotExist -> \"The specified finding does not exist.\"\n            | InvalidPromotionSetId -> \"The promotion set ID is invalid.\"\n            | InvalidFindingId -> \"The finding ID is invalid.\"\n            | InvalidReferenceId -> \"The reference ID is invalid.\"\n            | InvalidPolicySnapshotId -> \"The policy snapshot ID is invalid.\"\n            | InvalidResolutionState -> \"The resolution state is invalid.\"\n            | InvalidChapterId -> \"The chapter ID is invalid.\"\n\n        static member getErrorMessage(reviewError: ReviewError option) : string =\n            match reviewError with\n            | Some error -> ReviewError.getErrorMessage error\n            | None -> String.Empty\n\n    type QueueError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | QueueAlreadyInitialized\n        | QueueNotInitialized\n        | PromotionSetNotInQueue\n        | InvalidTargetBranchId\n        | InvalidPromotionSetId\n        | InvalidPolicySnapshotId\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(queueError: QueueError) : string =\n            match queueError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the queue event.\"\n            | QueueAlreadyInitialized -> \"The promotion queue is already initialized.\"\n            | QueueNotInitialized -> \"The promotion queue has not been initialized.\"\n            | PromotionSetNotInQueue -> \"The specified promotion set is not in the queue.\"\n            | InvalidTargetBranchId -> \"The target branch ID is invalid.\"\n            | InvalidPromotionSetId -> \"The promotion set ID is invalid.\"\n            | InvalidPolicySnapshotId -> \"The policy snapshot ID is invalid.\"\n\n        static member getErrorMessage(queueError: QueueError option) : string =\n            match queueError with\n            | Some error -> QueueError.getErrorMessage error\n            | None -> String.Empty\n\n    type ValidationSetError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | ValidationSetAlreadyExists\n        | ValidationSetDoesNotExist\n        | InvalidValidationSetId\n        | InvalidTargetBranchId\n        | ValidationSetRulesRequired\n        | ValidationDefinitionsRequired\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(validationSetError: ValidationSetError) : string =\n            match validationSetError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the validation set event.\"\n            | ValidationSetAlreadyExists -> \"A validation set with this ID already exists.\"\n            | ValidationSetDoesNotExist -> \"The validation set does not exist.\"\n            | InvalidValidationSetId -> \"The validation set ID is invalid.\"\n            | InvalidTargetBranchId -> \"The target branch ID is invalid.\"\n            | ValidationSetRulesRequired -> \"A validation set must include at least one rule.\"\n            | ValidationDefinitionsRequired -> \"A validation set must include at least one validation definition.\"\n\n        static member getErrorMessage(validationSetError: ValidationSetError option) : string =\n            match validationSetError with\n            | Some error -> ValidationSetError.getErrorMessage error\n            | None -> String.Empty\n\n    type ValidationResultError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | InvalidValidationResultId\n        | InvalidValidationSetId\n        | InvalidPromotionSetId\n        | InvalidPromotionSetStepId\n        | InvalidValidationStatus\n        | ValidationNameRequired\n        | ValidationVersionRequired\n        | InvalidArtifactId\n        | StepsComputationAttemptRequired\n        | InvalidStepsComputationAttempt\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(validationResultError: ValidationResultError) : string =\n            match validationResultError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the validation result event.\"\n            | InvalidValidationResultId -> \"The validation result ID is invalid.\"\n            | InvalidValidationSetId -> \"The validation set ID is invalid.\"\n            | InvalidPromotionSetId -> \"The promotion set ID is invalid.\"\n            | InvalidPromotionSetStepId -> \"The promotion set step ID is invalid.\"\n            | InvalidValidationStatus -> \"The validation status is invalid.\"\n            | ValidationNameRequired -> \"ValidationName is required.\"\n            | ValidationVersionRequired -> \"ValidationVersion is required.\"\n            | InvalidArtifactId -> \"One or more artifact IDs are invalid.\"\n            | StepsComputationAttemptRequired -> \"StepsComputationAttempt is required when PromotionSetId is provided.\"\n            | InvalidStepsComputationAttempt -> \"StepsComputationAttempt must be greater than zero.\"\n\n        static member getErrorMessage(validationResultError: ValidationResultError option) : string =\n            match validationResultError with\n            | Some error -> ValidationResultError.getErrorMessage error\n            | None -> String.Empty\n\n    type ArtifactError =\n        | DuplicateCorrelationId\n        | FailedWhileApplyingEvent\n        | ArtifactAlreadyExists\n        | ArtifactDoesNotExist\n        | InvalidArtifactId\n        | InvalidArtifactType\n        | InvalidMimeType\n        | InvalidSize\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(artifactError: ArtifactError) : string =\n            match artifactError with\n            | DuplicateCorrelationId -> \"A command with this correlation ID has already been processed.\"\n            | FailedWhileApplyingEvent -> \"An error occurred while processing the artifact event.\"\n            | ArtifactAlreadyExists -> \"The artifact already exists.\"\n            | ArtifactDoesNotExist -> \"The artifact does not exist.\"\n            | InvalidArtifactId -> \"The artifact ID is invalid.\"\n            | InvalidArtifactType -> \"The artifact type is invalid.\"\n            | InvalidMimeType -> \"The mime type is invalid.\"\n            | InvalidSize -> \"Artifact size must be zero or greater.\"\n\n        static member getErrorMessage(artifactError: ArtifactError option) : string =\n            match artifactError with\n            | Some error -> ArtifactError.getErrorMessage error\n            | None -> String.Empty\n\n    type TestError =\n        | TestFailed\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(testError: TestError) : string =\n            match testError with\n            | TestFailed -> getLocalizedString StringResourceName.TestFailed\n\n        static member getErrorMessage(testError: TestError option) : string =\n            match testError with\n            | Some error -> TestError.getErrorMessage error\n            | None -> String.Empty\n\n    type ReminderError =\n        | InvalidReminderDuration\n        | InvalidReminderTime\n        | InvalidReminderType\n        | ReminderActorIdIsRequired\n        | ReminderActorNameIsRequired\n        | ReminderDoesNotExist\n        | ReminderIdIsRequired\n\n        interface IErrorDiscriminatedUnion\n\n        static member getErrorMessage(reminderError: ReminderError) : string =\n            match reminderError with\n            | InvalidReminderDuration -> \"Invalid reminder duration. Use formats like '+15m', '+1h', '+1d'.\"\n            | InvalidReminderTime -> \"Invalid reminder time. Use ISO8601 format.\"\n            | InvalidReminderType -> \"Invalid reminder type. Valid types: Maintenance, PhysicalDeletion, DeleteCachedState, DeleteZipFile.\"\n            | ReminderActorIdIsRequired -> \"Actor ID is required for creating a reminder.\"\n            | ReminderActorNameIsRequired -> \"Actor name is required for creating a reminder.\"\n            | ReminderDoesNotExist -> \"The specified reminder does not exist.\"\n            | ReminderIdIsRequired -> \"Reminder ID is required.\"\n\n        static member getErrorMessage(reminderError: ReminderError option) : string =\n            match reminderError with\n            | Some error -> ReminderError.getErrorMessage error\n            | None -> String.Empty\n\n    /// Given an error object, returns the corresponding error message string.\n    let getErrorMessage<'T when 'T :> IErrorDiscriminatedUnion> (error: 'T) : string =\n        match box error with\n        | :? BranchError as branchError -> BranchError.getErrorMessage branchError\n        | :? ConfigError as configError -> ConfigError.getErrorMessage configError\n        | :? DiffError as diffError -> DiffError.getErrorMessage diffError\n        | :? DirectoryVersionError as directoryVersionError -> DirectoryVersionError.getErrorMessage directoryVersionError\n        | :? OwnerError as ownerError -> OwnerError.getErrorMessage ownerError\n        | :? OrganizationError as organizationError -> OrganizationError.getErrorMessage organizationError\n        | :? ReferenceError as referenceError -> ReferenceError.getErrorMessage referenceError\n        | :? ReminderError as reminderError -> ReminderError.getErrorMessage reminderError\n        | :? RepositoryError as repositoryError -> RepositoryError.getErrorMessage repositoryError\n        | :? StorageError as storageError -> StorageError.getErrorMessage storageError\n        | :? TestError as testError -> TestError.getErrorMessage testError\n        | :? WorkItemError as workItemError -> WorkItemError.getErrorMessage workItemError\n        | :? PolicyError as policyError -> PolicyError.getErrorMessage policyError\n        | :? ReviewError as reviewError -> ReviewError.getErrorMessage reviewError\n        | :? QueueError as queueError -> QueueError.getErrorMessage queueError\n        | :? ValidationSetError as validationSetError -> ValidationSetError.getErrorMessage validationSetError\n        | :? ValidationResultError as validationResultError -> ValidationResultError.getErrorMessage validationResultError\n        | :? ArtifactError as artifactError -> ArtifactError.getErrorMessage artifactError\n        | _ -> String.Empty\n\n    /// Given an optional error object, returns the corresponding error message string, or an empty string if None.\n    let getErrorOptionMessage<'T when 'T :> IErrorDiscriminatedUnion> (error: 'T option) : string =\n        match error with\n        | Some err -> getErrorMessage err\n        | None -> String.Empty\n"
  },
  {
    "path": "src/Grace.Shared/Validation/Repository.Validation.fs",
    "content": "namespace Grace.Shared.Validation\n\nopen Grace.Shared\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen Grace.Shared.Validation.Errors\nopen System\n\nmodule Repository =\n\n    /// Checks that the visibility value provided exists in the RepositoryVisibility type.\n    let visibilityIsValid (visibility: string) (error: RepositoryError) =\n        match Utilities.discriminatedUnionFromString<RepositoryType> (visibility) with\n        | Some visibility -> Ok() |> returnValueTask\n        | None -> Error error |> returnValueTask\n\n    /// Checks that the number of days is between 0.0 and 65536.0.\n    let daysIsValid (days: single) (error: RepositoryError) =\n        if days < 0.0f || days >= 65536.0f then\n            Error error |> returnValueTask\n        else\n            Ok() |> returnValueTask\n"
  },
  {
    "path": "src/Grace.Shared/Validation/Utilities.Validation.fs",
    "content": "namespace Grace.Shared.Validation\n\nopen FSharp.Control\nopen System.Threading.Tasks\nopen System\n\nmodule Utilities =\n\n    /// Returns the first validation that matches the predicate, or None if none match.\n    let tryFindOld<'T> (predicate: 'T -> bool) (validations: ValueTask<'T> array) =\n        task {\n            match validations\n                  |> Seq.tryFindIndex (fun validation -> predicate validation.Result)\n                with\n            | Some index -> return Some(validations[index].Result)\n            | None -> return None\n        }\n\n    /// Returns the first validation that matches the predicate, or None if none match.\n    let tryFind (predicate: 'T -> bool) (validations: ValueTask<'T> array) =\n        task {\n            let mutable i = 0\n            let mutable first = -1\n\n            while i < validations.Length && first = -1 do\n                let! result = validations[i]\n                if predicate result then first <- i\n                i <- i + 1\n\n            // Using .Result here is OK because it would already have been awaited in the while loop above.\n            if first >= 0 then return Some(validations[first].Result) else return None\n        }\n\n    /// Retrieves the first error from a list of validations.\n    let getFirstError (validations: ValueTask<Result<'T, 'TError>> array) =\n        task {\n            let! firstError =\n                validations\n                |> tryFind (fun validation -> Result.isError validation)\n\n            return\n                match firstError with\n                | Some result ->\n                    match result with\n                    | Ok _ -> None\n                    | Error error -> Some error // This line can't actually return None, because we'll always have an error if we get here.\n                | None -> None\n        }\n\n    /// Checks if any of a list of validations fail.\n    let anyFail validations =\n        task {\n            match! getFirstError validations with\n            | Some _ -> return true\n            | None -> return false\n        }\n\n    /// Checks that all validations in a list pass.\n    let allPass validations =\n        task {\n            let! anyFail = anyFail validations\n            return not anyFail\n        }\n"
  },
  {
    "path": "src/Grace.Types/AGENTS.md",
    "content": "# Grace.Types Agents Guide\n\nRefer to `../AGENTS.md` for shared expectations before working here.\n\n## Purpose\n\n- Domain types, discriminated unions, events, and DTOs used across actors, server, CLI, SDK, and persistence.\n- Canonical schema source for shared contracts.\n\n## Key Patterns\n\n- Prefer records and discriminated unions with clear intent.\n- Keep serializer attributes accurate (MessagePack, Orleans, System.Text.Json where applicable).\n- Keep domain contracts free of server endpoint concerns.\n- Synchronize schema changes with consumers in `Grace.Actors`, `Grace.Server`, `Grace.CLI`, and `Grace.SDK`.\n\n## Project Rules\n\n1. Breaking changes are allowed when the active specification explicitly requires them; update all consumers in the same change set.\n2. Keep type defaults deterministic and complete.\n3. Capture non-obvious schema rationale with succinct inline comments.\n\n## Validation\n\n- Add or update focused tests in `../Grace.Types.Tests` for changed type behavior.\n- Run `dotnet build --configuration Release` and affected test projects.\n"
  },
  {
    "path": "src/Grace.Types/Artifact.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Artifact =\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ArtifactType =\n        | AgentSummary\n        | ConflictReport\n        | Prompt\n        | ValidationOutput\n        | ReviewNotes\n        | Other of kind: string\n\n        static member GetKnownTypes() = GetKnownTypes<ArtifactType>()\n\n    [<GenerateSerializer>]\n    type ArtifactMetadata =\n        {\n            Class: string\n            ArtifactId: ArtifactId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            ArtifactType: ArtifactType\n            MimeType: string\n            Size: int64\n            Sha256: Sha256Hash option\n            BlobPath: string\n            CreatedAt: Instant\n            CreatedBy: UserId\n        }\n\n        static member Default =\n            {\n                Class = nameof ArtifactMetadata\n                ArtifactId = ArtifactId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                ArtifactType = ArtifactType.Other String.Empty\n                MimeType = String.Empty\n                Size = 0L\n                Sha256 = None\n                BlobPath = String.Empty\n                CreatedAt = Constants.DefaultTimestamp\n                CreatedBy = UserId String.Empty\n            }\n\n    [<GenerateSerializer>]\n    type ArtifactCreateResult = { ArtifactId: ArtifactId; UploadUri: UriWithSharedAccessSignature; BlobPath: string }\n\n    [<GenerateSerializer>]\n    type ArtifactDownloadUriResult = { ArtifactId: ArtifactId; DownloadUri: UriWithSharedAccessSignature }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ArtifactCommand =\n        | Create of artifact: ArtifactMetadata\n\n        static member GetKnownTypes() = GetKnownTypes<ArtifactCommand>()\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ArtifactEventType =\n        | Created of artifact: ArtifactMetadata\n\n        static member GetKnownTypes() = GetKnownTypes<ArtifactEventType>()\n\n    type ArtifactEvent = { Event: ArtifactEventType; Metadata: EventMetadata }\n\n    module ArtifactMetadata =\n        let UpdateDto (artifactEvent: ArtifactEvent) (_current: ArtifactMetadata) =\n            match artifactEvent.Event with\n            | ArtifactEventType.Created artifact -> artifact\n"
  },
  {
    "path": "src/Grace.Types/Auth.Types.fs",
    "content": "namespace Grace.Types\n\nopen Orleans\n\nmodule Auth =\n    [<GenerateSerializer>]\n    type OidcClientConfig =\n        {\n            [<Id 0u>]\n            Authority: string\n            [<Id 1u>]\n            Audience: string\n            [<Id 2u>]\n            CliClientId: string\n        }\n"
  },
  {
    "path": "src/Grace.Types/Authorization.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System.Runtime.Serialization\n\nmodule Authorization =\n\n    /// Internal principal identifier.\n    type PrincipalId = string\n\n    /// The type of principal represented in authorization decisions.\n    [<KnownType(\"GetKnownTypes\")>]\n    type PrincipalType =\n        | User\n        | Group\n        | Service\n\n        static member GetKnownTypes() = GetKnownTypes<PrincipalType>()\n\n    /// Identifies a principal (user, group, or service).\n    [<GenerateSerializer>]\n    type Principal =\n        {\n            [<Id 0u>]\n            PrincipalType: PrincipalType\n            [<Id 1u>]\n            PrincipalId: PrincipalId\n        }\n\n    /// Scope for role assignments in the resource hierarchy.\n    [<KnownType(\"GetKnownTypes\")>]\n    type Scope =\n        | [<Id 0u>] System\n        | [<Id 1u>] Owner of OwnerId\n        | [<Id 2u>] Organization of OwnerId * OrganizationId\n        | [<Id 3u>] Repository of OwnerId * OrganizationId * RepositoryId\n        | [<Id 4u>] Branch of OwnerId * OrganizationId * RepositoryId * BranchId\n\n        static member GetKnownTypes() = GetKnownTypes<Scope>()\n\n    /// Resource targeted by an authorization check.\n    [<KnownType(\"GetKnownTypes\")>]\n    type Resource =\n        | [<Id 0u>] System\n        | [<Id 1u>] Owner of OwnerId\n        | [<Id 2u>] Organization of OwnerId * OrganizationId\n        | [<Id 3u>] Repository of OwnerId * OrganizationId * RepositoryId\n        | [<Id 4u>] Branch of OwnerId * OrganizationId * RepositoryId * BranchId\n        | [<Id 5u>] Path of OwnerId * OrganizationId * RepositoryId * RelativePath\n\n        static member GetKnownTypes() = GetKnownTypes<Resource>()\n\n    /// Operation requested on a resource.\n    [<KnownType(\"GetKnownTypes\")>]\n    type Operation =\n        | SystemAdmin\n        | OwnerAdmin\n        | OwnerRead\n        | OrgAdmin\n        | OrgRead\n        | RepoAdmin\n        | RepoRead\n        | RepoWrite\n        | BranchAdmin\n        | BranchRead\n        | BranchWrite\n        | PathRead\n        | PathWrite\n\n        static member GetKnownTypes() = GetKnownTypes<Operation>()\n\n    /// Unique identifier for a role.\n    type RoleId = string\n\n    /// Defines a role's allowed operations and applicable scope kinds.\n    [<GenerateSerializer>]\n    type RoleDefinition =\n        {\n            [<Id 0u>]\n            RoleId: RoleId\n            [<Id 1u>]\n            AllowedOperations: Set<Operation>\n            [<Id 2u>]\n            AppliesTo: Set<string>\n        }\n\n    /// Binds a principal to a role at a specific scope.\n    [<GenerateSerializer>]\n    type RoleAssignment =\n        {\n            [<Id 0u>]\n            Principal: Principal\n            [<Id 1u>]\n            Scope: Scope\n            [<Id 2u>]\n            RoleId: RoleId\n            [<Id 3u>]\n            Source: string\n            [<Id 4u>]\n            SourceDetail: string option\n            [<Id 5u>]\n            CreatedAt: Instant\n        }\n\n    /// Result of an authorization check with a human-readable reason.\n    [<KnownType(\"GetKnownTypes\")>]\n    type PermissionCheckResult =\n        | [<Id 0u>] Allowed of string\n        | [<Id 1u>] Denied of string\n\n        static member GetKnownTypes() = GetKnownTypes<PermissionCheckResult>()\n\n    /// Commands for managing role assignments.\n    [<KnownType(\"GetKnownTypes\")>]\n    type AccessControlCommand =\n        | [<Id 0u>] GrantRole of RoleAssignment\n        | [<Id 1u>] RevokeRole of Principal * RoleId\n        | [<Id 2u>] ListAssignments of Principal option\n\n        static member GetKnownTypes() = GetKnownTypes<AccessControlCommand>()\n\n    /// Commands for managing repository path permissions.\n    [<KnownType(\"GetKnownTypes\")>]\n    type RepositoryPermissionCommand =\n        | [<Id 0u>] UpsertPathPermission of PathPermission\n        | [<Id 1u>] RemovePathPermission of RelativePath\n        | [<Id 2u>] ListPathPermissions of RelativePath option\n\n        static member GetKnownTypes() = GetKnownTypes<RepositoryPermissionCommand>()\n"
  },
  {
    "path": "src/Grace.Types/Automation.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Automation =\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type AutomationEventType =\n        | PromotionSetCreated\n        | PromotionSetUpdated\n        | PromotionSetEnqueued\n        | PromotionSetDequeued\n        | PromotionSetStepsUpdated\n        | PromotionSetRecomputeStarted\n        | PromotionSetRecomputeSucceeded\n        | PromotionSetRecomputeFailed\n        | PromotionSetApplyStarted\n        | PromotionSetApplied\n        | PromotionSetApplyFailed\n        | PromotionSetBlocked\n        | ValidationRequested\n        | ValidationResultRecorded\n        | ValidationSetCreated\n        | ValidationSetUpdated\n        | ArtifactCreated\n        | ReviewNotesUpdated\n        | ReviewCheckpointRecorded\n        | AgentWorkStarted\n        | AgentWorkStopped\n        | AgentSummaryAdded\n        | AgentBootstrapped\n\n        static member GetKnownTypes() = GetKnownTypes<AutomationEventType>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type AgentSessionLifecycleState =\n        | Inactive\n        | Active\n        | Stopping\n        | Stopped\n\n        static member GetKnownTypes() = GetKnownTypes<AgentSessionLifecycleState>()\n\n    [<GenerateSerializer>]\n    type AgentSessionInfo =\n        {\n            SessionId: string\n            AgentId: string\n            AgentDisplayName: string\n            WorkItemIdOrNumber: string\n            PromotionSetId: string\n            Source: string\n            LifecycleState: AgentSessionLifecycleState\n            StartedAt: Instant option\n            LastUpdatedAt: Instant option\n            StoppedAt: Instant option\n        }\n\n        static member Default =\n            {\n                SessionId = String.Empty\n                AgentId = String.Empty\n                AgentDisplayName = String.Empty\n                WorkItemIdOrNumber = String.Empty\n                PromotionSetId = String.Empty\n                Source = String.Empty\n                LifecycleState = AgentSessionLifecycleState.Inactive\n                StartedAt = None\n                LastUpdatedAt = None\n                StoppedAt = None\n            }\n\n    [<GenerateSerializer>]\n    type AgentSessionOperationResult =\n        {\n            Session: AgentSessionInfo\n            Message: string\n            OperationId: string\n            WasIdempotentReplay: bool\n        }\n\n        static member Default =\n            {\n                Session = AgentSessionInfo.Default\n                Message = String.Empty\n                OperationId = String.Empty\n                WasIdempotentReplay = false\n            }\n\n    [<GenerateSerializer>]\n    type AgentSessionListResult =\n        {\n            Sessions: AgentSessionInfo list\n            Count: int\n            Message: string\n        }\n\n        static member Default =\n            {\n                Sessions = []\n                Count = 0\n                Message = String.Empty\n            }\n\n    [<GenerateSerializer>]\n    type AutomationEventEnvelope =\n        {\n            EventId: Guid\n            EventType: AutomationEventType\n            EventTime: Instant\n            CorrelationId: CorrelationId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            ActorId: string\n            DataJson: string\n        }\n\n        static member Create\n            (eventType: AutomationEventType)\n            (eventTime: Instant)\n            (correlationId: CorrelationId)\n            (ownerId: OwnerId)\n            (organizationId: OrganizationId)\n            (repositoryId: RepositoryId)\n            (actorId: string)\n            (dataJson: string)\n            =\n            {\n                EventId = Guid.NewGuid()\n                EventType = eventType\n                EventTime = eventTime\n                CorrelationId = correlationId\n                OwnerId = ownerId\n                OrganizationId = organizationId\n                RepositoryId = repositoryId\n                ActorId = actorId\n                DataJson = dataJson\n            }\n"
  },
  {
    "path": "src/Grace.Types/Branch.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Reference\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\nopen System.Collections\n\nmodule Branch =\n\n    /// The state held in the database when creating a physical deletion reminder for a branch.\n    type PhysicalDeletionReminderState =\n        {\n            RepositoryId: RepositoryId\n            BranchId: BranchId\n            BranchName: BranchName\n            ParentBranchId: ParentBranchId\n            DeleteReason: DeleteReason\n            CorrelationId: CorrelationId\n        }\n\n    /// Defines the commands for the Branch actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type BranchCommand =\n        | Create of\n            branchId: BranchId *\n            branchName: BranchName *\n            parentBranchId: BranchId *\n            basedOn: ReferenceId *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            repositoryId: RepositoryId *\n            initialPermissions: ReferenceType seq\n        | Rebase of basedOn: ReferenceId\n        | SetName of newName: BranchName\n        | Assign of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Promote of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Commit of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Checkpoint of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Save of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Tag of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | CreateExternal of directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | EnableAssign of enabled: bool\n        | EnablePromotion of enabled: bool\n        | EnableCommit of enabled: bool\n        | EnableCheckpoint of enabled: bool\n        | EnableSave of enabled: bool\n        | EnableTag of enabled: bool\n        | EnableExternal of enabled: bool\n        | EnableAutoRebase of enabled: bool\n        | SetPromotionMode of promotionMode: BranchPromotionMode\n        | RemoveReference of referenceId: ReferenceId\n        | UpdateParentBranch of newParentBranchId: BranchId\n        | DeleteLogical of force: bool * DeleteReason: DeleteReason * reassignChildBranches: bool * newParentBranchId: BranchId option\n        | DeletePhysical\n        | Undelete\n\n        static member GetKnownTypes() = GetKnownTypes<BranchCommand>()\n\n    /// Defines the events for the Branch actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type BranchEventType =\n        | Created of\n            branchId: BranchId *\n            branchName: BranchName *\n            parentBranchId: BranchId *\n            basedOn: ReferenceId *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            repositoryId: RepositoryId *\n            initialPermissions: ReferenceType seq\n        | Rebased of basedOn: ReferenceId\n        | NameSet of newName: BranchName\n        | Assigned of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Promoted of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Committed of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Checkpointed of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Saved of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | Tagged of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | ExternalCreated of referenceDto: ReferenceDto * directoryVersionId: DirectoryVersionId * sha256Hash: Sha256Hash * referenceText: ReferenceText\n        | EnabledAssign of enabled: bool\n        | EnabledPromotion of enabled: bool\n        | EnabledCommit of enabled: bool\n        | EnabledCheckpoint of enabled: bool\n        | EnabledSave of enabled: bool\n        | EnabledTag of enabled: bool\n        | EnabledExternal of enabled: bool\n        | EnabledAutoRebase of enabled: bool\n        | PromotionModeSet of promotionMode: BranchPromotionMode\n        | ReferenceRemoved of referenceId: ReferenceId\n        | ParentBranchUpdated of newParentBranchId: BranchId\n        | LogicalDeleted of force: bool * DeleteReason: DeleteReason * reassignedChildBranches: bool * childrenReassignedTo: BranchId option\n        | PhysicalDeleted\n        | Undeleted\n\n        static member GetKnownTypes() = GetKnownTypes<BranchEventType>()\n\n    /// Record that holds the event type and metadata for a Branch event.\n    type BranchEvent =\n        {\n            /// The BranchEventType case that describes the event.\n            Event: BranchEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// The BranchDto is a data transfer object that represents a branch in the system.\n    type BranchDto =\n        {\n            Class: string\n            BranchId: BranchId\n            BranchName: BranchName\n            ParentBranchId: BranchId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            BasedOn: ReferenceDto\n            UserId: UserId\n            AssignEnabled: bool\n            PromotionEnabled: bool\n            CommitEnabled: bool\n            CheckpointEnabled: bool\n            SaveEnabled: bool\n            TagEnabled: bool\n            ExternalEnabled: bool\n            AutoRebaseEnabled: bool\n            PromotionMode: BranchPromotionMode\n            LatestReference: ReferenceDto\n            LatestPromotion: ReferenceDto\n            LatestCommit: ReferenceDto\n            LatestCheckpoint: ReferenceDto\n            LatestSave: ReferenceDto\n            ShouldRecomputeLatestReferences: bool\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        static member Default =\n            {\n                Class = nameof BranchDto\n                BranchId = BranchId.Empty\n                BranchName = BranchName \"root\"\n                ParentBranchId = Constants.DefaultParentBranchId\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                BasedOn = ReferenceDto.Default\n                UserId = UserId String.Empty\n                AssignEnabled = false\n                PromotionEnabled = false\n                CommitEnabled = false\n                CheckpointEnabled = false\n                SaveEnabled = false\n                TagEnabled = false\n                ExternalEnabled = false\n                AutoRebaseEnabled = true\n                PromotionMode = BranchPromotionMode.IndividualOnly\n                LatestReference = ReferenceDto.Default\n                LatestPromotion = ReferenceDto.Default\n                LatestCommit = ReferenceDto.Default\n                LatestCheckpoint = ReferenceDto.Default\n                LatestSave = ReferenceDto.Default\n                ShouldRecomputeLatestReferences = true\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n                DeletedAt = None\n                DeleteReason = String.Empty\n            }\n\n        static member UpdateDto branchEvent currentBranchDto =\n            let branchEventType = branchEvent.Event\n\n            let newBranchDto =\n                match branchEventType with\n                | Created (branchId, branchName, parentBranchId, basedOn, ownerId, organizationId, repositoryId, initialPermissions) ->\n                    let basedOnReferenceDto =\n                        deserialize<ReferenceDto> (\n                            branchEvent\n                                .Metadata\n                                .Properties[ \"basedOnReferenceDto\" ]\n                                .ToString()\n                        )\n\n                    let mutable branchDto =\n                        { BranchDto.Default with\n                            BranchId = branchId\n                            BranchName = branchName\n                            ParentBranchId = parentBranchId\n                            BasedOn = basedOnReferenceDto\n                            OwnerId = ownerId\n                            OrganizationId = organizationId\n                            RepositoryId = repositoryId\n                            CreatedAt = branchEvent.Metadata.Timestamp\n                        }\n\n                    for referenceType in initialPermissions do\n                        branchDto <-\n                            match referenceType with\n                            | ReferenceType.Promotion -> { branchDto with PromotionEnabled = true }\n                            | ReferenceType.Commit -> { branchDto with CommitEnabled = true }\n                            | ReferenceType.Checkpoint -> { branchDto with CheckpointEnabled = true }\n                            | ReferenceType.Save -> { branchDto with SaveEnabled = true }\n                            | ReferenceType.Tag -> { branchDto with TagEnabled = true }\n                            | ReferenceType.External -> { branchDto with ExternalEnabled = true }\n                            | ReferenceType.Rebase -> branchDto // Rebase is always allowed. (Auto-rebase is optional, but rebase itself is always allowed.)\n\n                    branchDto\n                | Rebased referenceId ->\n                    let basedOnReferenceDto =\n                        deserialize<ReferenceDto> (\n                            branchEvent\n                                .Metadata\n                                .Properties[ \"basedOnReferenceDto\" ]\n                                .ToString()\n                        )\n\n                    { currentBranchDto with BasedOn = basedOnReferenceDto }\n                | NameSet branchName -> { currentBranchDto with BranchName = branchName }\n                | Assigned (referenceDto, directoryVersion, sha256Hash, referenceText) ->\n                    { currentBranchDto with LatestPromotion = referenceDto; BasedOn = referenceDto; ShouldRecomputeLatestReferences = true }\n\n                | Promoted (referenceDto, directoryVersion, sha256Hash, referenceText) ->\n                    { currentBranchDto with LatestPromotion = referenceDto; BasedOn = referenceDto; ShouldRecomputeLatestReferences = true }\n\n                | Committed (referenceDto, directoryVersion, sha256Hash, referenceText) ->\n                    { currentBranchDto with LatestCommit = referenceDto; ShouldRecomputeLatestReferences = true }\n\n                | Checkpointed (referenceDto, directoryVersion, sha256Hash, referenceText) ->\n                    { currentBranchDto with LatestCheckpoint = referenceDto; ShouldRecomputeLatestReferences = true }\n\n                | Saved (referenceDto, directoryVersion, sha256Hash, referenceText) ->\n                    { currentBranchDto with LatestSave = referenceDto; ShouldRecomputeLatestReferences = true }\n\n                | Tagged (referenceDto, directoryVersion, sha256Hash, referenceText) -> { currentBranchDto with ShouldRecomputeLatestReferences = true }\n                | ExternalCreated (referenceDto, directoryVersion, sha256Hash, referenceText) ->\n                    { currentBranchDto with ShouldRecomputeLatestReferences = true }\n                | EnabledAssign enabled -> { currentBranchDto with AssignEnabled = enabled }\n                | EnabledPromotion enabled -> { currentBranchDto with PromotionEnabled = enabled }\n                | EnabledCommit enabled -> { currentBranchDto with CommitEnabled = enabled }\n                | EnabledCheckpoint enabled -> { currentBranchDto with CheckpointEnabled = enabled }\n                | EnabledSave enabled -> { currentBranchDto with SaveEnabled = enabled }\n                | EnabledTag enabled -> { currentBranchDto with TagEnabled = enabled }\n                | EnabledExternal enabled -> { currentBranchDto with ExternalEnabled = enabled }\n                | EnabledAutoRebase enabled -> { currentBranchDto with AutoRebaseEnabled = enabled }\n                | PromotionModeSet promotionMode -> { currentBranchDto with PromotionMode = promotionMode }\n                | ReferenceRemoved _ -> currentBranchDto\n                | ParentBranchUpdated newParentBranchId -> { currentBranchDto with ParentBranchId = newParentBranchId }\n                | LogicalDeleted (force, deleteReason, reassignedChildBranches, childrenReassignedTo) ->\n                    { currentBranchDto with DeletedAt = Some(getCurrentInstant ()); DeleteReason = deleteReason }\n\n                | PhysicalDeleted -> currentBranchDto // Do nothing because it's about to be deleted anyway.\n                | Undeleted -> { currentBranchDto with DeletedAt = None; DeleteReason = String.Empty }\n\n\n            { newBranchDto with UpdatedAt = Some branchEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/Diff.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System.Collections.Generic\nopen System.Runtime.Serialization\n\nmodule Diff =\n\n    /// The state held in the database when creating a physical deletion reminder for a cached state.\n    type DeleteCachedStateReminderState = { DeleteReason: DeleteReason; CorrelationId: CorrelationId }\n\n    /// Represents a Diff between two DirectoryVersions in a repository.\n    //[<GenerateSerializer>]\n    type DiffDto =\n        {\n            Class: string\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            DirectoryVersionId1: DirectoryVersionId\n            Directory1CreatedAt: Instant\n            DirectoryVersionId2: DirectoryVersionId\n            Directory2CreatedAt: Instant\n            HasDifferences: bool\n            Differences: List<FileSystemDifference>\n            FileDiffs: List<FileDiff>\n        }\n\n        static member Default =\n            {\n                Class = nameof DiffDto\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                DirectoryVersionId1 = DirectoryVersionId.Empty\n                Directory1CreatedAt = Constants.DefaultTimestamp\n                DirectoryVersionId2 = DirectoryVersionId.Empty\n                Directory2CreatedAt = Constants.DefaultTimestamp\n                Differences = List<FileSystemDifference>()\n                HasDifferences = false\n                FileDiffs = List<FileDiff>()\n            }\n"
  },
  {
    "path": "src/Grace.Types/DirectoryVersion.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Repository\nopen Grace.Types.Types\nopen NodaTime\nopen MessagePack\nopen Orleans\nopen System\nopen System.Collections.Generic\nopen System.Runtime.Serialization\n\nmodule DirectoryVersion =\n\n    /// The state held in the database when creating a physical deletion reminder for a DirectoryVersion.\n    type PhysicalDeletionReminderState = { DeleteReason: DeleteReason; CorrelationId: CorrelationId }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type DirectoryVersionCommand =\n        | Create of directoryVersion: DirectoryVersion * repositoryDto: RepositoryDto\n        | SetRecursiveSize of recursizeSize: int64\n        | DeleteLogical of DeleteReason: DeleteReason\n        | DeletePhysical\n        | Undelete\n\n        static member GetKnownTypes() = GetKnownTypes<DirectoryVersionCommand>()\n\n    /// Defines the events for the DirectoryVersion actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type DirectoryVersionEventType =\n        | Created of directoryVersion: DirectoryVersion\n        | RecursiveSizeSet of recursiveSize: int64\n        | LogicalDeleted of DeleteReason: DeleteReason\n        | PhysicalDeleted\n        | Undeleted\n\n    /// Record that holds the event type and metadata for a DirectoryVersion event.\n    type DirectoryVersionEvent =\n        {\n            /// The DirectoryVersionEventType case that describes the event.\n            Event: DirectoryVersionEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// The DirectoryVersionDto is a data transfer object that represents a directory version in the system.\n    [<MessagePackObject>]\n    type DirectoryVersionDto =\n        {\n            [<Key(0)>]\n            DirectoryVersion: DirectoryVersion\n            [<Key(1)>]\n            RecursiveSize: int64\n            [<Key(2)>]\n            DeletedAt: Instant option\n            [<Key(3)>]\n            DeleteReason: DeleteReason\n            [<Key(4)>]\n            HashesValidated: bool\n        }\n\n        static member Default =\n            {\n                DirectoryVersion = DirectoryVersion.Default\n                RecursiveSize = Constants.InitialDirectorySize\n                DeletedAt = None\n                DeleteReason = String.Empty\n                HashesValidated = false\n            }\n\n        static member UpdateDto directoryVersionEvent currentDirectoryVersionDto =\n            let directoryVersionEventType = directoryVersionEvent.Event\n\n            match directoryVersionEventType with\n            | Created directoryVersion -> { currentDirectoryVersionDto with DirectoryVersion = directoryVersion }\n            | RecursiveSizeSet recursiveSize -> { currentDirectoryVersionDto with RecursiveSize = recursiveSize }\n            | LogicalDeleted deleteReason -> { currentDirectoryVersionDto with DeletedAt = Some(getCurrentInstant ()); DeleteReason = deleteReason }\n            | PhysicalDeleted -> currentDirectoryVersionDto // Do nothing because it's about to be deleted anyway.\n            | Undeleted -> { currentDirectoryVersionDto with DeletedAt = None; DeleteReason = String.Empty }\n"
  },
  {
    "path": "src/Grace.Types/Events.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Types\nopen Grace.Types.Types\nopen Grace.Shared.Utilities\nopen NodaTime\nopen Orleans\nopen System.Runtime.Serialization\nopen Grace.Types.WorkItem\nopen Grace.Types.Policy\nopen Grace.Types.Review\nopen Grace.Types.Queue\nopen Grace.Types.PromotionSet\nopen Grace.Types.Validation\nopen Grace.Types.Artifact\n\nmodule Events =\n\n    /// A discriminated union that holds all of the possible events for Grace. Used for publishing events to graceEventStream.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type GraceEvent =\n        | OwnerEvent of Owner.OwnerEvent\n        | BranchEvent of Branch.BranchEvent\n        | DirectoryVersionEvent of DirectoryVersion.DirectoryVersionEvent\n        | OrganizationEvent of Organization.OrganizationEvent\n        | ReferenceEvent of Reference.ReferenceEvent\n        | RepositoryEvent of Repository.RepositoryEvent\n        | WorkItemEvent of WorkItem.WorkItemEvent\n        | PolicyEvent of Policy.PolicyEvent\n        | ReviewEvent of Review.ReviewEvent\n        | QueueEvent of Queue.PromotionQueueEvent\n        | PromotionSetEvent of PromotionSet.PromotionSetEvent\n        | ValidationSetEvent of Validation.ValidationSetEvent\n        | ValidationResultEvent of Validation.ValidationResultEvent\n        | ArtifactEvent of Artifact.ArtifactEvent\n\n        static member GetKnownTypes() = GetKnownTypes<GraceEvent>()\n\n        override this.ToString() =\n            match this with\n            | OwnerEvent e -> serialize e\n            | BranchEvent e -> serialize e\n            | DirectoryVersionEvent e -> serialize e\n            | OrganizationEvent e -> serialize e\n            | ReferenceEvent e -> serialize e\n            | RepositoryEvent e -> serialize e\n            | WorkItemEvent e -> serialize e\n            | PolicyEvent e -> serialize e\n            | ReviewEvent e -> serialize e\n            | QueueEvent e -> serialize e\n            | PromotionSetEvent e -> serialize e\n            | ValidationSetEvent e -> serialize e\n            | ValidationResultEvent e -> serialize e\n            | ArtifactEvent e -> serialize e\n"
  },
  {
    "path": "src/Grace.Types/Grace.Types.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>net10.0</TargetFramework>\n        <LangVersion>Preview</LangVersion>\n        <PublishReadyToRun>true</PublishReadyToRun>\n        <Version>0.1</Version>\n        <Description>The shared types module for Grace.</Description>\n        <GenerateDocumentationFile>true</GenerateDocumentationFile>\n        <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n        <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    </PropertyGroup>\n\n    <ItemGroup>\n      <EmbeddedResource Remove=\"Actors\\**\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <None Include=\"instructions.md\" />\n        <Compile Include=\"..\\Grace.Shared\\Constants.Shared.fs\" Link=\"Constants.Shared.fs\" />\n        <Compile Include=\"..\\Grace.Shared\\Utilities.Shared.fs\" Link=\"Utilities.Shared.fs\" />\n        <Compile Include=\"Types.Types.fs\" />\n        <Compile Include=\"Authorization.Types.fs\" />\n        <Compile Include=\"Auth.Types.fs\" />\n        <Compile Include=\"PersonalAccessToken.Types.fs\" />\n        <Compile Include=\"Owner.Types.fs\" />\n        <Compile Include=\"Organization.Types.fs\" />\n        <Compile Include=\"Repository.Types.fs\" />\n        <Compile Include=\"Reference.Types.fs\" />\n        <Compile Include=\"Branch.Types.fs\" />\n        \n        <Compile Include=\"PromotionSet.Types.fs\" />\n        <Compile Include=\"WorkItem.Types.fs\" />\n        <Compile Include=\"Policy.Types.fs\" />\n        <Compile Include=\"RequiredAction.Types.fs\" />\n        <Compile Include=\"Review.Types.fs\" />\n        <Compile Include=\"Queue.Types.fs\" />\n        <Compile Include=\"Artifact.Types.fs\" />\n        <Compile Include=\"Automation.Types.fs\" />\n        <Compile Include=\"Validation.Types.fs\" />\n        <Compile Include=\"Diff.Types.fs\" />\n        <Compile Include=\"DirectoryVersion.Types.fs\" />\n        <Compile Include=\"Reminder.Types.fs\" />\n        <Compile Include=\"Events.Types.fs\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"Ben.Demystifier\" Version=\"0.4.1\" />\n        <PackageReference Include=\"DiffPlex\" Version=\"1.9.0\" />\n        <PackageReference Include=\"FSharp.Control.TaskSeq\" Version=\"0.4.0\" />\n        <PackageReference Include=\"FSharp.SystemTextJson\" Version=\"1.4.36\" />\n        <PackageReference Include=\"FSharpPlus\" Version=\"1.8.0\" />\n        <PackageReference Include=\"MessagePack\" Version=\"3.1.4\" />\n        <PackageReference Include=\"MessagePack.Annotations\" Version=\"3.1.4\" />\n        <PackageReference Include=\"MessagePack.FSharpExtensions\" Version=\"4.0.0\" />\n        <PackageReference Include=\"MessagePack.NodaTime\" Version=\"3.5.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"10.0.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.Configuration.Abstractions\" Version=\"10.0.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.ObjectPool\" Version=\"10.0.0\" />\n        <PackageReference Include=\"MimeTypeMapOfficial\" Version=\"1.0.17\" />\n        <PackageReference Include=\"MimeTypes\" Version=\"2.5.2\" />\n        <PackageReference Include=\"Nanoid\" Version=\"3.1.0\" />\n        <PackageReference Include=\"NodaTime\" Version=\"3.2.2\" />\n        <PackageReference Include=\"NodaTime.Serialization.SystemTextJson\" Version=\"1.3.0\" />\n        <PackageReference Include=\"Orleans.Serialization.NodaTime\" Version=\"0.0.4-beta\" />\n        <PackageReference Include=\"Polly\" Version=\"8.6.5\" />\n        <PackageReference Include=\"Polly.Contrib.WaitAndRetry\" Version=\"1.1.1\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Grace.Types/Organization.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\n/// Defines commands, events, and types for Organizations.\nmodule Organization =\n\n    /// The state held in the database when creating a physical deletion reminder for an organization.\n    type PhysicalDeletionReminderState = { DeleteReason: DeleteReason; CorrelationId: CorrelationId }\n\n    /// Defines the commands for the Organization actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type OrganizationCommand =\n        | Create of organizationId: OrganizationId * organizationName: OrganizationName * ownerId: OwnerId\n        | SetName of organizationName: OrganizationName\n        | SetType of organizationType: OrganizationType\n        | SetSearchVisibility of searchVisibility: SearchVisibility\n        | SetDescription of description: string\n        | DeleteLogical of force: bool * DeleteReason: DeleteReason\n        | DeletePhysical\n        | Undelete\n\n        static member GetKnownTypes() = GetKnownTypes<OrganizationCommand>()\n\n    /// Defines the events for the Organization actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type OrganizationEventType =\n        | Created of organizationId: OrganizationId * organizationName: OrganizationName * ownerId: OwnerId\n        | NameSet of organizationName: OrganizationName\n        | TypeSet of organizationType: OrganizationType\n        | SearchVisibilitySet of searchVisibility: SearchVisibility\n        | DescriptionSet of organizationDescription: string\n        | LogicalDeleted of force: bool * DeleteReason: DeleteReason\n        | PhysicalDeleted\n        | Undeleted\n\n        static member GetKnownTypes() = GetKnownTypes<OrganizationEventType>()\n\n    /// Record that holds the event type and metadata for an Organization event.\n    type OrganizationEvent =\n        {\n            /// The OrganizationEventType case that describes the event.\n            Event: OrganizationEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    type OrganizationDto =\n        {\n            Class: string\n            OrganizationId: OrganizationId\n            OrganizationName: OrganizationName\n            OwnerId: OwnerId\n            OrganizationType: OrganizationType\n            Description: string\n            SearchVisibility: SearchVisibility\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        static member Default =\n            {\n                Class = nameof OrganizationDto\n                OrganizationId = OrganizationId.Empty\n                OrganizationName = String.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationType = OrganizationType.Public\n                Description = String.Empty\n                SearchVisibility = Visible\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n                DeletedAt = None\n                DeleteReason = String.Empty\n            }\n\n        /// Updates the OrganizationDto based on the OrganizationEvent.\n        static member UpdateDto organizationEvent currentOrganizationDto =\n            let newOrganizationDto =\n                match organizationEvent.Event with\n                | Created (organizationId, organizationName, ownerId) ->\n                    { OrganizationDto.Default with\n                        OrganizationId = organizationId\n                        OrganizationName = organizationName\n                        OwnerId = ownerId\n                        CreatedAt = organizationEvent.Metadata.Timestamp\n                    }\n                | NameSet (organizationName) -> { currentOrganizationDto with OrganizationName = organizationName }\n                | TypeSet (organizationType) -> { currentOrganizationDto with OrganizationType = organizationType }\n                | SearchVisibilitySet (searchVisibility) -> { currentOrganizationDto with SearchVisibility = searchVisibility }\n                | DescriptionSet (description) -> { currentOrganizationDto with Description = description }\n                | LogicalDeleted (_, deleteReason) -> { currentOrganizationDto with DeleteReason = deleteReason; DeletedAt = Some(getCurrentInstant ()) }\n                | PhysicalDeleted -> currentOrganizationDto // Do nothing because it's about to be deleted anyway.\n                | Undeleted -> { currentOrganizationDto with DeletedAt = None; DeleteReason = String.Empty }\n\n            { newOrganizationDto with UpdatedAt = Some organizationEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/Owner.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\n/// Defines commands, events, and types for the Owner actor.\nmodule Owner =\n\n    /// The state held in the database when creating a physical deletion reminder for an owner.\n    type PhysicalDeletionReminderState = { DeleteReason: DeleteReason; CorrelationId: CorrelationId }\n\n    /// Defines the commands for the Owner actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type OwnerCommand =\n        | Create of ownerId: OwnerId * ownerName: OwnerName\n        | SetName of ownerName: OwnerName\n        | SetType of ownerType: OwnerType\n        | SetSearchVisibility of searchVisibility: SearchVisibility\n        | SetDescription of description: string\n        | DeleteLogical of force: bool * DeleteReason: DeleteReason\n        | DeletePhysical\n        | Undelete\n\n        static member GetKnownTypes() = GetKnownTypes<OwnerCommand>()\n\n    /// Defines the events for the Owner actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type OwnerEventType =\n        | Created of ownerId: OwnerId * ownerName: OwnerName\n        | NameSet of ownerName: OwnerName\n        | TypeSet of ownerType: OwnerType\n        | SearchVisibilitySet of searchVisibility: SearchVisibility\n        | DescriptionSet of description: string\n        | LogicalDeleted of force: bool * DeleteReason: DeleteReason\n        | PhysicalDeleted\n        | Undeleted\n\n        static member GetKnownTypes() = GetKnownTypes<OwnerEventType>()\n\n    /// Record that holds the event type and metadata for an Owner event.\n    type OwnerEvent =\n        {\n            /// The OwnerEventType case that describes the event.\n            Event: OwnerEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// The OwnerDto is a data transfer object that represents an owner in the system.\n    type OwnerDto =\n        {\n            Class: string\n            OwnerId: OwnerId\n            OwnerName: OwnerName\n            OwnerType: OwnerType\n            Description: string\n            SearchVisibility: SearchVisibility\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        /// Default instance of OwnerDto with empty or default values.\n        static member Default =\n            {\n                Class = nameof OwnerDto\n                OwnerId = OwnerId.Empty\n                OwnerName = String.Empty\n                OwnerType = OwnerType.Public\n                Description = String.Empty\n                SearchVisibility = Visible\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n                DeletedAt = None\n                DeleteReason = String.Empty\n            }\n\n        /// Updates the OwnerDto based on the OwnerEvent received.\n        static member UpdateDto ownerEvent currentOwnerDto =\n            let newOwnerDto =\n                match ownerEvent.Event with\n                | Created (ownerId, ownerName) -> { OwnerDto.Default with OwnerId = ownerId; OwnerName = ownerName; CreatedAt = ownerEvent.Metadata.Timestamp }\n                | NameSet (ownerName) -> { currentOwnerDto with OwnerName = ownerName }\n                | TypeSet (ownerType) -> { currentOwnerDto with OwnerType = ownerType }\n                | SearchVisibilitySet (searchVisibility) -> { currentOwnerDto with SearchVisibility = searchVisibility }\n                | DescriptionSet (description) -> { currentOwnerDto with Description = description }\n                | LogicalDeleted (_, deleteReason) -> { currentOwnerDto with DeletedAt = Some(getCurrentInstant ()); DeleteReason = deleteReason }\n                | PhysicalDeleted -> currentOwnerDto // Do nothing because it's about to be deleted anyway.\n                | Undeleted -> { currentOwnerDto with DeletedAt = None; DeleteReason = String.Empty }\n\n            { newOwnerDto with UpdatedAt = Some ownerEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/PersonalAccessToken.Types.fs",
    "content": "namespace Grace.Types\n\nopen NodaTime\nopen Orleans\nopen System\nopen System.Text\n\nmodule PersonalAccessToken =\n    [<Literal>]\n    let TokenPrefix = \"grace_pat_v1_\"\n\n    type PersonalAccessTokenId = Guid\n\n    [<GenerateSerializer>]\n    type PersonalAccessTokenSummary =\n        {\n            [<Id 0u>]\n            TokenId: PersonalAccessTokenId\n            [<Id 1u>]\n            Name: string\n            [<Id 2u>]\n            CreatedAt: Instant\n            [<Id 3u>]\n            ExpiresAt: Instant option\n            [<Id 4u>]\n            LastUsedAt: Instant option\n            [<Id 5u>]\n            RevokedAt: Instant option\n        }\n\n    [<GenerateSerializer>]\n    type PersonalAccessTokenCreated =\n        {\n            [<Id 0u>]\n            Token: string\n            [<Id 1u>]\n            Summary: PersonalAccessTokenSummary\n        }\n\n    [<GenerateSerializer>]\n    type PersonalAccessTokenValidationResult =\n        {\n            [<Id 0u>]\n            TokenId: PersonalAccessTokenId\n            [<Id 1u>]\n            UserId: Types.UserId\n            [<Id 2u>]\n            Claims: string list\n            [<Id 3u>]\n            GroupIds: string list\n        }\n\n    let private base64UrlEncode (bytes: byte array) =\n        Convert\n            .ToBase64String(bytes)\n            .TrimEnd('=')\n            .Replace('+', '-')\n            .Replace('/', '_')\n\n    let private tryBase64UrlDecode (value: string) =\n        try\n            let normalized = value.Replace('-', '+').Replace('_', '/')\n            let padding = (4 - (normalized.Length % 4)) % 4\n            let padded = normalized + String.replicate padding \"=\"\n            Some(Convert.FromBase64String(padded))\n        with\n        | _ -> None\n\n    let formatToken (userId: string) (tokenId: Guid) (secret: byte array) =\n        let userIdBytes = Encoding.UTF8.GetBytes(userId)\n        let userIdB64 = base64UrlEncode userIdBytes\n        let tokenIdN = tokenId.ToString(\"N\")\n        let secretB64 = base64UrlEncode secret\n        $\"{TokenPrefix}{userIdB64}.{tokenIdN}.{secretB64}\"\n\n    let tryParseToken (token: string) =\n        if String.IsNullOrWhiteSpace token then\n            None\n        else if not (token.StartsWith(TokenPrefix, StringComparison.Ordinal)) then\n            None\n        else\n            let payload = token.Substring(TokenPrefix.Length)\n            let parts = payload.Split('.', StringSplitOptions.RemoveEmptyEntries)\n\n            if parts.Length <> 3 then\n                None\n            else\n                let userIdPart = parts[0]\n                let tokenIdPart = parts[1]\n                let secretPart = parts[2]\n\n                match Guid.TryParseExact(tokenIdPart, \"N\") with\n                | true, tokenId ->\n                    match tryBase64UrlDecode userIdPart, tryBase64UrlDecode secretPart with\n                    | Some userIdBytes, Some secretBytes ->\n                        let userId = Encoding.UTF8.GetString(userIdBytes)\n\n                        if String.IsNullOrWhiteSpace userId\n                           || secretBytes.Length <> 32 then\n                            None\n                        else\n                            Some(userId, tokenId, secretBytes)\n                    | _ -> None\n                | _ -> None\n"
  },
  {
    "path": "src/Grace.Types/Policy.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Policy =\n    /// The Id of the policy snapshot (hash of contents + parser version).\n    type PolicySnapshotId = Sha256Hash\n\n    /// Controls token budgets for model analysis stages.\n    [<GenerateSerializer>]\n    type PolicyAnalysisSettings = { Enabled: bool; MaxTokens: int }\n\n    /// Rules used to determine whether Stage 0 is non-trivial.\n    [<GenerateSerializer>]\n    type PolicyNonTrivialSignal = { ChurnLinesThreshold: int; TouchedSensitivePaths: bool; DependencyConfigChanges: bool; ApiSurfaceChanges: bool }\n\n    /// Default policy values.\n    [<GenerateSerializer>]\n    type PolicyDefaults =\n        {\n            RequireHumanReview: bool\n            DeepAnalysis: PolicyAnalysisSettings\n            Triage: PolicyAnalysisSettings\n            NonTrivialSignal: PolicyNonTrivialSignal\n        }\n\n    /// Rule for policy triggers.\n    [<GenerateSerializer>]\n    type PolicyRule =\n        {\n            PathMatches: string option\n            FileMatches: string option\n            ApiSurfaceChanged: bool option\n            DependencyChanged: bool option\n            ConfigChanged: bool option\n        }\n\n    /// Redaction configuration for evidence extraction.\n    [<GenerateSerializer>]\n    type PolicyRedaction = { Enabled: bool; Patterns: string list; DenylistPaths: string list }\n\n    /// Baseline drift thresholds for re-acknowledgement.\n    [<GenerateSerializer>]\n    type BaselineDriftThreshold = { ChurnLines: int; FilesTouched: int }\n\n    /// Policy rules for approvals.\n    [<GenerateSerializer>]\n    type PolicyApprovalRules = { BaselineDriftReackThreshold: BaselineDriftThreshold }\n\n    /// Defines queue behavior after a failure.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type QueueFailureAction =\n        | PauseQueue\n        | Continue\n        | QuarantinePromotionSet\n\n        static member GetKnownTypes() = GetKnownTypes<QueueFailureAction>()\n\n    /// Queue-related policy configuration.\n    [<GenerateSerializer>]\n    type PolicyQueueRules = { OnFailure: QueueFailureAction }\n\n    /// Compiled policy ruleset.\n    [<GenerateSerializer>]\n    type PolicyRuleset =\n        {\n            Defaults: PolicyDefaults\n            HumanReviewRequiredWhen: PolicyRule list\n            DeepAnalysisRequiredWhen: PolicyRule list\n            SensitivePaths: string list\n            Redaction: PolicyRedaction\n            ApprovalRules: PolicyApprovalRules\n            Queue: PolicyQueueRules\n        }\n\n    /// Immutable snapshot of the compiled policy.\n    [<GenerateSerializer>]\n    type PolicySnapshot =\n        {\n            Class: string\n            PolicySnapshotId: PolicySnapshotId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            TargetBranchId: BranchId\n            PolicyVersion: int\n            ParserVersion: string\n            SourceHash: Sha256Hash\n            Rules: PolicyRuleset\n            CreatedAt: Instant\n        }\n\n        static member Default =\n            {\n                Class = nameof PolicySnapshot\n                PolicySnapshotId = Sha256Hash String.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                TargetBranchId = BranchId.Empty\n                PolicyVersion = 1\n                ParserVersion = String.Empty\n                SourceHash = Sha256Hash String.Empty\n                Rules =\n                    {\n                        Defaults =\n                            {\n                                RequireHumanReview = true\n                                DeepAnalysis = { Enabled = true; MaxTokens = 12000 }\n                                Triage = { Enabled = true; MaxTokens = 2500 }\n                                NonTrivialSignal =\n                                    { ChurnLinesThreshold = 80; TouchedSensitivePaths = true; DependencyConfigChanges = true; ApiSurfaceChanges = true }\n                            }\n                        HumanReviewRequiredWhen = []\n                        DeepAnalysisRequiredWhen = []\n                        SensitivePaths = []\n                        Redaction = { Enabled = false; Patterns = []; DenylistPaths = [] }\n                        ApprovalRules = { BaselineDriftReackThreshold = { ChurnLines = 50; FilesTouched = 5 } }\n                        Queue = { OnFailure = QueueFailureAction.PauseQueue }\n                    }\n                CreatedAt = Constants.DefaultTimestamp\n            }\n\n    /// Acknowledgement of a policy snapshot.\n    [<GenerateSerializer>]\n    type PolicyAcknowledgement = { PolicySnapshotId: PolicySnapshotId; AcknowledgedBy: UserId; AcknowledgedAt: Instant; Note: string option }\n\n    /// Defines the commands for the Policy actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type PolicyCommand =\n        | CreateSnapshot of policySnapshot: PolicySnapshot\n        | Acknowledge of policySnapshotId: PolicySnapshotId * acknowledgedBy: UserId * note: string option\n\n        static member GetKnownTypes() = GetKnownTypes<PolicyCommand>()\n\n    /// Defines the events for the Policy actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type PolicyEventType =\n        | SnapshotCreated of policySnapshot: PolicySnapshot\n        | Acknowledged of policySnapshotId: PolicySnapshotId * acknowledgedBy: UserId * note: string option\n\n        static member GetKnownTypes() = GetKnownTypes<PolicyEventType>()\n\n    /// Record that holds the event type and metadata for a Policy event.\n    type PolicyEvent =\n        {\n            /// The PolicyEventType case that describes the event.\n            Event: PolicyEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n"
  },
  {
    "path": "src/Grace.Types/PromotionSet.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen Microsoft.Extensions.Configuration\nopen NodaTime\nopen Orleans\nopen System\nopen System.Collections.Generic\nopen System.Net.Http\nopen System.Net.Http.Headers\nopen System.Runtime.Serialization\nopen System.Text\nopen System.Text.Json\nopen System.Threading.Tasks\n\nmodule PromotionSet =\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type PromotionSetStatus =\n        | Ready\n        | Running\n        | Succeeded\n        | Failed\n        | Blocked\n\n        static member GetKnownTypes() = GetKnownTypes<PromotionSetStatus>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type StepsComputationStatus =\n        | NotComputed\n        | Computing\n        | Computed\n        | ComputeFailed\n\n        static member GetKnownTypes() = GetKnownTypes<StepsComputationStatus>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type StepConflictStatus =\n        | NoConflicts\n        | AutoResolved\n        | BlockedPendingReview\n        | Failed\n\n        static member GetKnownTypes() = GetKnownTypes<StepConflictStatus>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ConflictResolutionMethod =\n        | None\n        | ModelSuggested\n        | ManualOverride\n\n        static member GetKnownTypes() = GetKnownTypes<ConflictResolutionMethod>()\n\n    [<GenerateSerializer>]\n    type ConflictResolutionOutcome = { ModelResolution: string; Confidence: float; Accepted: bool option }\n\n    [<GenerateSerializer>]\n    type ConflictHunk = { StartLine: int; EndLine: int; OursContent: string; TheirsContent: string }\n\n    [<GenerateSerializer>]\n    type ConflictAnalysis =\n        {\n            FilePath: string\n            OriginalHunks: ConflictHunk list\n            ProposedResolution: ConflictResolutionOutcome option\n            ResolutionMethod: ConflictResolutionMethod\n        }\n\n    [<GenerateSerializer>]\n    type ConflictResolutionDecision = { FilePath: string; Accepted: bool; OverrideContentArtifactId: ArtifactId option }\n\n    [<GenerateSerializer>]\n    type PromotionPointer = { BranchId: BranchId; ReferenceId: ReferenceId; DirectoryVersionId: DirectoryVersionId }\n\n    [<GenerateSerializer>]\n    type PromotionSetStep =\n        {\n            StepId: PromotionSetStepId\n            Order: int\n            OriginalPromotion: PromotionPointer\n            OriginalBasePromotionReferenceId: ReferenceId\n            OriginalBaseDirectoryVersionId: DirectoryVersionId\n            ComputedAgainstBaseDirectoryVersionId: DirectoryVersionId\n            AppliedDirectoryVersionId: DirectoryVersionId\n            ConflictSummaryArtifactId: ArtifactId option\n            ConflictStatus: StepConflictStatus\n        }\n\n    [<GenerateSerializer>]\n    type PromotionSetDto =\n        {\n            Class: string\n            PromotionSetId: PromotionSetId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            TargetBranchId: BranchId\n            OnBehalfOf: UserId list\n            Steps: PromotionSetStep list\n            ComputedAgainstParentTerminalPromotionReferenceId: ReferenceId option\n            StepsComputationStatus: StepsComputationStatus\n            StepsComputationAttempt: int\n            StepsComputationError: string option\n            StepsComputationUpdatedAt: Instant option\n            Status: PromotionSetStatus\n            CreatedBy: UserId\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        static member Default =\n            {\n                Class = nameof PromotionSetDto\n                PromotionSetId = PromotionSetId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                TargetBranchId = BranchId.Empty\n                OnBehalfOf = []\n                Steps = []\n                ComputedAgainstParentTerminalPromotionReferenceId = Option.None\n                StepsComputationStatus = StepsComputationStatus.NotComputed\n                StepsComputationAttempt = 0\n                StepsComputationError = Option.None\n                StepsComputationUpdatedAt = Option.None\n                Status = PromotionSetStatus.Ready\n                CreatedBy = UserId String.Empty\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = Option.None\n                DeletedAt = Option.None\n                DeleteReason = String.Empty\n            }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type PromotionSetCommand =\n        | CreatePromotionSet of\n            promotionSetId: PromotionSetId *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            repositoryId: RepositoryId *\n            targetBranchId: BranchId\n        | UpdateInputPromotions of promotionPointers: PromotionPointer list\n        | RecomputeStepsIfStale of reason: string option\n        | ResolveConflicts of stepId: PromotionSetStepId * resolutions: ConflictResolutionDecision list\n        | Apply\n        | DeleteLogical of force: bool * deleteReason: DeleteReason\n\n        static member GetKnownTypes() = GetKnownTypes<PromotionSetCommand>()\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type PromotionSetEventType =\n        | Created of promotionSetId: PromotionSetId * ownerId: OwnerId * organizationId: OrganizationId * repositoryId: RepositoryId * targetBranchId: BranchId\n        | InputPromotionsUpdated of promotionPointers: PromotionPointer list\n        | RecomputeStarted of computedAgainstTerminal: ReferenceId\n        | StepsUpdated of steps: PromotionSetStep list * computedAgainstTerminal: ReferenceId\n        | RecomputeFailed of reason: string * computedAgainstTerminal: ReferenceId\n        | Blocked of reason: string * artifactId: ArtifactId option\n        | ApplyStarted\n        | Applied of terminalPromotionReferenceId: ReferenceId\n        | ApplyFailed of reason: string\n        | LogicalDeleted of force: bool * deleteReason: DeleteReason\n\n        static member GetKnownTypes() = GetKnownTypes<PromotionSetEventType>()\n\n    type PromotionSetEvent = { Event: PromotionSetEventType; Metadata: EventMetadata }\n\n    module PromotionSetDto =\n        let UpdateDto (promotionSetEvent: PromotionSetEvent) (currentDto: PromotionSetDto) =\n            let updatedDto, shouldUpdateComputationTimestamp =\n                match promotionSetEvent.Event with\n                | Created (promotionSetId, ownerId, organizationId, repositoryId, targetBranchId) ->\n                    { PromotionSetDto.Default with\n                        PromotionSetId = promotionSetId\n                        OwnerId = ownerId\n                        OrganizationId = organizationId\n                        RepositoryId = repositoryId\n                        TargetBranchId = targetBranchId\n                        CreatedBy = UserId promotionSetEvent.Metadata.Principal\n                        CreatedAt = promotionSetEvent.Metadata.Timestamp\n                    },\n                    false\n                | InputPromotionsUpdated promotionPointers ->\n                    let steps =\n                        promotionPointers\n                        |> List.mapi (fun index pointer ->\n                            {\n                                StepId = Guid.NewGuid()\n                                Order = index\n                                OriginalPromotion = pointer\n                                OriginalBasePromotionReferenceId = ReferenceId.Empty\n                                OriginalBaseDirectoryVersionId = DirectoryVersionId.Empty\n                                ComputedAgainstBaseDirectoryVersionId = DirectoryVersionId.Empty\n                                AppliedDirectoryVersionId = DirectoryVersionId.Empty\n                                ConflictSummaryArtifactId = Option.None\n                                ConflictStatus = StepConflictStatus.NoConflicts\n                            })\n\n                    { currentDto with\n                        Steps = steps\n                        Status = PromotionSetStatus.Ready\n                        StepsComputationStatus = StepsComputationStatus.NotComputed\n                        ComputedAgainstParentTerminalPromotionReferenceId = Option.None\n                        StepsComputationError = Option.None\n                    },\n                    true\n                | RecomputeStarted computedAgainstTerminal ->\n                    { currentDto with\n                        Status = PromotionSetStatus.Running\n                        StepsComputationStatus = StepsComputationStatus.Computing\n                        ComputedAgainstParentTerminalPromotionReferenceId = Some computedAgainstTerminal\n                        StepsComputationError = Option.None\n                    },\n                    true\n                | StepsUpdated (steps, computedAgainstTerminal) ->\n                    { currentDto with\n                        Steps = steps\n                        Status = PromotionSetStatus.Ready\n                        StepsComputationStatus = StepsComputationStatus.Computed\n                        ComputedAgainstParentTerminalPromotionReferenceId = Some computedAgainstTerminal\n                        StepsComputationAttempt = currentDto.StepsComputationAttempt + 1\n                        StepsComputationError = Option.None\n                    },\n                    true\n                | RecomputeFailed (reason, computedAgainstTerminal) ->\n                    { currentDto with\n                        Status = PromotionSetStatus.Failed\n                        StepsComputationStatus = StepsComputationStatus.ComputeFailed\n                        ComputedAgainstParentTerminalPromotionReferenceId = Some computedAgainstTerminal\n                        StepsComputationError = Some reason\n                    },\n                    true\n                | Blocked (reason, _) ->\n                    { currentDto with\n                        Status = PromotionSetStatus.Blocked\n                        StepsComputationStatus = StepsComputationStatus.ComputeFailed\n                        StepsComputationError = Some reason\n                    },\n                    true\n                | ApplyStarted -> { currentDto with Status = PromotionSetStatus.Running }, false\n                | Applied _ -> { currentDto with Status = PromotionSetStatus.Succeeded }, false\n                | ApplyFailed reason -> { currentDto with Status = PromotionSetStatus.Failed; StepsComputationError = Some reason }, false\n                | LogicalDeleted (_, deleteReason) -> { currentDto with DeletedAt = Some(getCurrentInstant ()); DeleteReason = deleteReason }, false\n\n            let onBehalfOf =\n                updatedDto.OnBehalfOf\n                |> List.append [ UserId promotionSetEvent.Metadata.Principal ]\n                |> List.distinct\n\n            let computationUpdatedAt =\n                if shouldUpdateComputationTimestamp then\n                    Some promotionSetEvent.Metadata.Timestamp\n                else\n                    updatedDto.StepsComputationUpdatedAt\n\n            { updatedDto with OnBehalfOf = onBehalfOf; UpdatedAt = Some promotionSetEvent.Metadata.Timestamp; StepsComputationUpdatedAt = computationUpdatedAt }\n\nmodule PromotionSetConflictModel =\n\n    [<CLIMutable>]\n    type ConflictResolutionModelRequest = { FilePath: string; BaseContent: string option; OursContent: string option; TheirsContent: string option }\n\n    [<CLIMutable>]\n    type ConflictResolutionModelResponse = { ProposedContent: string option; ShouldDelete: bool; Confidence: float; Explanation: string option }\n\n    type IConflictResolutionModelProvider =\n        abstract member ProviderName: string\n        abstract member SuggestResolution: ConflictResolutionModelRequest -> Task<Result<ConflictResolutionModelResponse, string>>\n\n    type OpenRouterSettings = { ApiBase: string; ApiKeyEnvVar: string; Model: string; RequestHeaders: Dictionary<string, string> option }\n\n    type PromotionSetModelsSettings = { Provider: string; OpenRouter: OpenRouterSettings }\n\n    let private toOption (value: string) = if String.IsNullOrWhiteSpace value then Option.None else Option.Some value\n\n    let private truncateForPrompt (content: string option) =\n        let maxLength = 12000\n\n        content\n        |> Option.map (fun text ->\n            if String.IsNullOrEmpty text then text\n            elif text.Length <= maxLength then text\n            else text[.. (maxLength - 1)])\n\n    let private buildPrompt (request: ConflictResolutionModelRequest) =\n        let baseContent =\n            truncateForPrompt request.BaseContent\n            |> Option.defaultValue \"<none>\"\n\n        let oursContent =\n            truncateForPrompt request.OursContent\n            |> Option.defaultValue \"<none>\"\n\n        let theirsContent =\n            truncateForPrompt request.TheirsContent\n            |> Option.defaultValue \"<none>\"\n\n        String.Join(\n            Environment.NewLine,\n            [|\n                $\"File path: {request.FilePath}\"\n                String.Empty\n                \"Resolve this merge conflict and return ONLY JSON with this exact schema:\"\n                \"{\"\n                \"  \\\"proposedContent\\\": string|null,\"\n                \"  \\\"shouldDelete\\\": boolean,\"\n                \"  \\\"confidence\\\": number,\"\n                \"  \\\"explanation\\\": string\"\n                \"}\"\n                String.Empty\n                \"Rules:\"\n                \"- confidence must be between 0.0 and 1.0.\"\n                \"- use shouldDelete=true only when file should be deleted.\"\n                \"- when shouldDelete=false, proposedContent must contain full merged file content.\"\n                \"- do not include markdown code fences.\"\n                String.Empty\n                \"BASE:\"\n                baseContent\n                String.Empty\n                \"OURS:\"\n                oursContent\n                String.Empty\n                \"THEIRS:\"\n                theirsContent\n            |]\n        )\n\n    let private tryExtractJsonPayload (content: string) =\n        if String.IsNullOrWhiteSpace content then\n            Option.None\n        else\n            let trimmed = content.Trim()\n\n            if trimmed.StartsWith(\"{\", StringComparison.Ordinal) then\n                Option.Some trimmed\n            else\n                let firstBrace = trimmed.IndexOf('{')\n                let lastBrace = trimmed.LastIndexOf('}')\n\n                if firstBrace >= 0 && lastBrace > firstBrace then\n                    Option.Some(trimmed.Substring(firstBrace, lastBrace - firstBrace + 1))\n                else\n                    Option.None\n\n    let private tryGetProperty (name: string) (jsonElement: JsonElement) =\n        let mutable propertyElement = Unchecked.defaultof<JsonElement>\n\n        if jsonElement.TryGetProperty(name, &propertyElement) then\n            Option.Some propertyElement\n        else\n            Option.None\n\n    let tryParseModelResponse (content: string) =\n        match tryExtractJsonPayload content with\n        | Option.None -> Error \"Model response did not contain a JSON payload.\"\n        | Option.Some jsonPayload ->\n            try\n                use payloadDocument = JsonDocument.Parse(jsonPayload)\n                let rootElement = payloadDocument.RootElement\n\n                let confidence =\n                    match tryGetProperty \"confidence\" rootElement with\n                    | Option.Some confidenceElement when confidenceElement.ValueKind = JsonValueKind.Number ->\n                        let mutable parsedConfidence = 0.0\n\n                        if confidenceElement.TryGetDouble(&parsedConfidence) then\n                            Ok parsedConfidence\n                        else\n                            Error \"Model response confidence could not be parsed.\"\n                    | _ -> Error \"Model response is missing numeric confidence.\"\n\n                let proposedContentResult =\n                    match tryGetProperty \"proposedContent\" rootElement with\n                    | Option.Some proposedContentElement when proposedContentElement.ValueKind = JsonValueKind.Null -> Ok Option.None\n                    | Option.Some proposedContentElement when proposedContentElement.ValueKind = JsonValueKind.String ->\n                        Ok(Option.Some(proposedContentElement.GetString()))\n                    | Option.Some _ -> Error \"Model response proposedContent must be string or null.\"\n                    | Option.None -> Ok Option.None\n\n                let shouldDeleteResult =\n                    match tryGetProperty \"shouldDelete\" rootElement with\n                    | Option.Some shouldDeleteElement when shouldDeleteElement.ValueKind = JsonValueKind.True -> Ok true\n                    | Option.Some shouldDeleteElement when shouldDeleteElement.ValueKind = JsonValueKind.False -> Ok false\n                    | _ -> Ok false\n\n                let explanation =\n                    match tryGetProperty \"explanation\" rootElement with\n                    | Option.Some explanationElement when explanationElement.ValueKind = JsonValueKind.String -> toOption (explanationElement.GetString())\n                    | _ -> Option.None\n\n                match confidence, proposedContentResult, shouldDeleteResult with\n                | Ok parsedConfidence, Ok proposedContent, Ok shouldDelete ->\n                    if Double.IsNaN parsedConfidence\n                       || Double.IsInfinity parsedConfidence then\n                        Error \"Model response confidence must be finite.\"\n                    elif parsedConfidence < 0.0 || parsedConfidence > 1.0 then\n                        Error \"Model response confidence must be in range [0.0, 1.0].\"\n                    elif not shouldDelete && proposedContent.IsNone then\n                        Error \"Model response proposedContent is required when shouldDelete is false.\"\n                    else\n                        Ok { ProposedContent = proposedContent; ShouldDelete = shouldDelete; Confidence = parsedConfidence; Explanation = explanation }\n                | Error errorText, _, _\n                | _, Error errorText, _\n                | _, _, Error errorText -> Error errorText\n            with\n            | ex -> Error $\"Failed to parse model response JSON: {ex.Message}\"\n\n    type NullConflictResolutionModelProvider() =\n        interface IConflictResolutionModelProvider with\n            member _.ProviderName = \"none\"\n            member _.SuggestResolution _ = Task.FromResult(Error \"Conflict resolution model provider is not configured.\")\n\n    type OpenRouterConflictResolutionModelProvider(settings: OpenRouterSettings) =\n        let httpClient = new HttpClient()\n\n        let apiBase =\n            if String.IsNullOrWhiteSpace settings.ApiBase then\n                \"https://openrouter.ai/api/v1\"\n            else\n                settings.ApiBase.TrimEnd('/')\n\n        let requestUri = Uri($\"{apiBase}/chat/completions\")\n\n        interface IConflictResolutionModelProvider with\n            member _.ProviderName = \"OpenRouter\"\n\n            member _.SuggestResolution(request: ConflictResolutionModelRequest) =\n                task {\n                    let apiKey = Environment.GetEnvironmentVariable(settings.ApiKeyEnvVar)\n\n                    if String.IsNullOrWhiteSpace apiKey then\n                        return Error $\"Conflict resolution model API key is not configured in environment variable '{settings.ApiKeyEnvVar}'.\"\n                    else\n                        try\n                            let payload =\n                                {|\n                                    model = settings.Model\n                                    temperature = 0.0\n                                    messages =\n                                        [|\n                                            {|\n                                                role = \"system\"\n                                                content = \"You are a merge-conflict resolver. Return strict JSON only and no additional text.\"\n                                            |}\n                                            {| role = \"user\"; content = buildPrompt request |}\n                                        |]\n                                |}\n\n                            let payloadJson = JsonSerializer.Serialize(payload)\n                            use requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri)\n                            requestMessage.Headers.Authorization <- AuthenticationHeaderValue(\"Bearer\", apiKey)\n\n                            settings.RequestHeaders\n                            |> Option.defaultValue (Dictionary<string, string>())\n                            |> Seq.iter (fun header ->\n                                if not <| String.IsNullOrWhiteSpace header.Key then\n                                    requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value)\n                                    |> ignore)\n\n                            requestMessage.Content <- new StringContent(payloadJson, Encoding.UTF8, \"application/json\")\n\n                            use! responseMessage = httpClient.SendAsync(requestMessage)\n                            let! responseBody = responseMessage.Content.ReadAsStringAsync()\n\n                            if not responseMessage.IsSuccessStatusCode then\n                                return\n                                    Error(\n                                        $\"Conflict resolution model request failed with status {(int responseMessage.StatusCode)} ({responseMessage.ReasonPhrase}).\"\n                                    )\n                            else\n                                try\n                                    use responseDocument = JsonDocument.Parse(responseBody)\n                                    let rootElement = responseDocument.RootElement\n                                    let choicesElement = rootElement.GetProperty(\"choices\")\n\n                                    if choicesElement.GetArrayLength() = 0 then\n                                        return Error \"Conflict resolution model response did not include choices.\"\n                                    else\n                                        let firstChoiceElement = choicesElement[0]\n                                        let messageElement = firstChoiceElement.GetProperty(\"message\")\n                                        let content = messageElement.GetProperty(\"content\").GetString()\n\n                                        match tryParseModelResponse content with\n                                        | Ok parsedResponse -> return Ok parsedResponse\n                                        | Error errorText -> return Error errorText\n                                with\n                                | ex -> return Error($\"Conflict resolution model response was malformed and could not be parsed: {ex.Message}\")\n                        with\n                        | ex -> return Error $\"Conflict resolution model request failed: {ex.Message}\"\n                }\n\n    let private tryGetSettings (configuration: IConfiguration) =\n        if isNull configuration then\n            Option.None\n        else\n            let promotionSetModelsSection = configuration.GetSection(\"Grace:PromotionSetModels\")\n\n            if isNull promotionSetModelsSection then\n                Option.None\n            else\n                let openRouterSection = promotionSetModelsSection.GetSection(\"OpenRouter\")\n                let requestHeadersSection = openRouterSection.GetSection(\"RequestHeaders\")\n\n                let requestHeaders =\n                    if isNull requestHeadersSection then\n                        Option.None\n                    else\n                        let headers = Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)\n\n                        requestHeadersSection.GetChildren()\n                        |> Seq.iter (fun section ->\n                            if not <| String.IsNullOrWhiteSpace section.Key then\n                                headers[section.Key] <- section.Value)\n\n                        if headers.Count = 0 then Option.None else Option.Some headers\n\n                Option.Some\n                    {\n                        Provider = promotionSetModelsSection[\"Provider\"]\n                        OpenRouter =\n                            {\n                                ApiBase = openRouterSection[\"ApiBase\"]\n                                ApiKeyEnvVar = openRouterSection[\"ApiKeyEnvVar\"]\n                                Model = openRouterSection[\"Model\"]\n                                RequestHeaders = requestHeaders\n                            }\n                    }\n\n    let createProvider (configuration: IConfiguration) =\n        match tryGetSettings configuration with\n        | Option.Some settings when not <| String.IsNullOrWhiteSpace settings.Provider ->\n            match settings.Provider.Trim() with\n            | \"OpenRouter\" when\n                not\n                <| String.IsNullOrWhiteSpace settings.OpenRouter.Model\n                ->\n                OpenRouterConflictResolutionModelProvider(settings.OpenRouter) :> IConflictResolutionModelProvider\n            | _ -> NullConflictResolutionModelProvider() :> IConflictResolutionModelProvider\n        | _ -> NullConflictResolutionModelProvider() :> IConflictResolutionModelProvider\n"
  },
  {
    "path": "src/Grace.Types/Queue.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System.Runtime.Serialization\n\nmodule Queue =\n    /// Queue state for a target branch.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type QueueState =\n        | Idle\n        | Running\n        | Paused\n        | Degraded\n\n        static member GetKnownTypes() = GetKnownTypes<QueueState>()\n\n    /// Promotion queue for a target branch.\n    [<GenerateSerializer>]\n    type PromotionQueue =\n        {\n            Class: string\n            TargetBranchId: BranchId\n            PromotionSetIds: PromotionSetId list\n            RunningPromotionSetId: PromotionSetId option\n            State: QueueState\n            PolicySnapshotId: PolicySnapshotId\n            UpdatedAt: Instant option\n        }\n\n        static member Default =\n            {\n                Class = nameof PromotionQueue\n                TargetBranchId = BranchId.Empty\n                PromotionSetIds = []\n                RunningPromotionSetId = None\n                State = QueueState.Idle\n                PolicySnapshotId = PolicySnapshotId \"\"\n                UpdatedAt = None\n            }\n\n    /// Defines the commands for the PromotionQueue actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type PromotionQueueCommand =\n        | Initialize of targetBranchId: BranchId * policySnapshotId: PolicySnapshotId\n        | Enqueue of promotionSetId: PromotionSetId\n        | Dequeue of promotionSetId: PromotionSetId\n        | SetRunning of promotionSetId: PromotionSetId option\n        | Pause\n        | Resume\n        | SetDegraded\n        | UpdatePolicySnapshot of policySnapshotId: PolicySnapshotId\n\n        static member GetKnownTypes() = GetKnownTypes<PromotionQueueCommand>()\n\n    /// Defines the events for the PromotionQueue actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type PromotionQueueEventType =\n        | Initialized of targetBranchId: BranchId * policySnapshotId: PolicySnapshotId\n        | PromotionSetEnqueued of promotionSetId: PromotionSetId\n        | PromotionSetDequeued of promotionSetId: PromotionSetId\n        | RunningPromotionSetSet of promotionSetId: PromotionSetId option\n        | Paused\n        | Resumed\n        | Degraded\n        | PolicySnapshotUpdated of policySnapshotId: PolicySnapshotId\n\n        static member GetKnownTypes() = GetKnownTypes<PromotionQueueEventType>()\n\n    /// Record that holds the event type and metadata for a PromotionQueue event.\n    type PromotionQueueEvent =\n        {\n            /// The PromotionQueueEventType case that describes the event.\n            Event: PromotionQueueEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// Updates the PromotionQueue based on the PromotionQueueEvent.\n    module PromotionQueueDto =\n        let UpdateDto (promotionQueueEvent: PromotionQueueEvent) (currentQueue: PromotionQueue) =\n            let newQueue =\n                match promotionQueueEvent.Event with\n                | Initialized (targetBranchId, policySnapshotId) ->\n                    { PromotionQueue.Default with TargetBranchId = targetBranchId; PolicySnapshotId = policySnapshotId }\n                | PromotionSetEnqueued promotionSetId -> { currentQueue with PromotionSetIds = currentQueue.PromotionSetIds @ [ promotionSetId ] }\n                | PromotionSetDequeued promotionSetId ->\n                    { currentQueue with\n                        PromotionSetIds =\n                            currentQueue.PromotionSetIds\n                            |> List.filter (fun existing -> existing <> promotionSetId)\n                    }\n                | RunningPromotionSetSet promotionSetId -> { currentQueue with RunningPromotionSetId = promotionSetId; State = QueueState.Running }\n                | Paused -> { currentQueue with State = QueueState.Paused }\n                | Resumed ->\n                    let newState =\n                        match currentQueue.RunningPromotionSetId with\n                        | Some _ -> QueueState.Running\n                        | None -> QueueState.Idle\n\n                    { currentQueue with State = newState }\n                | Degraded -> { currentQueue with State = QueueState.Degraded }\n                | PolicySnapshotUpdated policySnapshotId -> { currentQueue with PolicySnapshotId = policySnapshotId }\n\n            { newQueue with UpdatedAt = Some promotionQueueEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/Reference.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Reference =\n\n    /// The state held in the database when creating a physical deletion reminder for a reference.\n    [<GenerateSerializer>]\n    type PhysicalDeletionReminderState =\n        {\n            RepositoryId: RepositoryId\n            BranchId: BranchId\n            DirectoryVersionId: DirectoryVersionId\n            Sha256Hash: Sha256Hash\n            DeleteReason: DeleteReason\n            CorrelationId: CorrelationId\n        }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ReferenceCommand =\n        | Create of\n            ReferenceId: ReferenceId *\n            OwnerId: OwnerId *\n            OrganizationId: OrganizationId *\n            RepositoryId: RepositoryId *\n            BranchId: BranchId *\n            DirectoryId: DirectoryVersionId *\n            Sha256Hash: Sha256Hash *\n            ReferenceType: ReferenceType *\n            ReferenceText: ReferenceText *\n            Links: ReferenceLinkType seq\n        | AddLink of link: ReferenceLinkType\n        | RemoveLink of link: ReferenceLinkType\n        | DeleteLogical of force: bool * DeleteReason: DeleteReason\n        | DeletePhysical\n        | Undelete\n\n        static member GetKnownTypes() = GetKnownTypes<ReferenceCommand>()\n\n    /// Defines the events for the Reference actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type ReferenceEventType =\n        | Created of\n            ReferenceId: ReferenceId *\n            OwnerId: OwnerId *\n            OrganizationId: OrganizationId *\n            RepositoryId: RepositoryId *\n            BranchId: BranchId *\n            DirectoryId: DirectoryVersionId *\n            Sha256Hash: Sha256Hash *\n            ReferenceType: ReferenceType *\n            ReferenceText: ReferenceText *\n            Links: ReferenceLinkType seq\n        | LinkAdded of link: ReferenceLinkType\n        | LinkRemoved of link: ReferenceLinkType\n        | LogicalDeleted of force: bool * DeleteReason: DeleteReason\n        | PhysicalDeleted\n        | Undeleted\n\n        static member GetKnownTypes() = GetKnownTypes<ReferenceEventType>()\n\n    /// Record that holds the event type and metadata for a Reference event.\n    type ReferenceEvent =\n        {\n            /// The ReferenceEventType case that describes the event.\n            Event: ReferenceEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// The ReferenceDto is a data transfer object that represents a reference in the system.\n    type ReferenceDto =\n        {\n            Class: string\n            ReferenceId: ReferenceId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            BranchId: BranchId\n            DirectoryId: DirectoryVersionId\n            Sha256Hash: Sha256Hash\n            ReferenceType: ReferenceType\n            ReferenceText: ReferenceText\n            Links: ReferenceLinkType seq\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        static member Default =\n            {\n                Class = nameof ReferenceDto\n                ReferenceId = ReferenceId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                BranchId = BranchId.Empty\n                DirectoryId = DirectoryVersionId.Empty\n                Sha256Hash = Sha256Hash String.Empty\n                ReferenceType = Save\n                ReferenceText = ReferenceText String.Empty\n                Links = Seq.empty\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n                DeletedAt = None\n                DeleteReason = String.Empty\n            }\n\n        /// Updates the ReferenceDto based on the ReferenceEvent.\n        static member UpdateDto referenceEvent currentReferenceDto =\n            let newReferenceDto =\n                match referenceEvent.Event with\n                | Created (referenceId, ownerId, organizationId, repositoryId, branchId, directoryId, sha256Hash, referenceType, referenceText, links) ->\n                    { currentReferenceDto with\n                        ReferenceId = referenceId\n                        OwnerId = ownerId\n                        OrganizationId = organizationId\n                        RepositoryId = repositoryId\n                        BranchId = branchId\n                        DirectoryId = directoryId\n                        Sha256Hash = sha256Hash\n                        ReferenceType = referenceType\n                        ReferenceText = referenceText\n                        Links = links\n                        CreatedAt = referenceEvent.Metadata.Timestamp\n                    }\n                | LinkAdded link ->\n                    { currentReferenceDto with\n                        Links =\n                            currentReferenceDto.Links\n                            |> Seq.append (Seq.singleton link)\n                            |> Seq.distinct\n                    }\n                | LinkRemoved link ->\n                    { currentReferenceDto with\n                        Links =\n                            currentReferenceDto.Links\n                            |> Seq.except (Seq.singleton link)\n                    }\n                | LogicalDeleted (force, deleteReason) -> { currentReferenceDto with DeletedAt = Some(getCurrentInstant ()); DeleteReason = deleteReason }\n                | PhysicalDeleted -> currentReferenceDto // Do nothing because it's about to be deleted anyway.\n                | Undeleted -> { currentReferenceDto with DeletedAt = None; DeleteReason = String.Empty }\n\n            { newReferenceDto with UpdatedAt = Some referenceEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/Reminder.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared.Utilities\nopen Grace.Types.Branch\nopen Grace.Types.Diff\nopen Grace.Types.DirectoryVersion\nopen Grace.Types.Organization\nopen Grace.Types.Owner\nopen Grace.Types.Reference\nopen Grace.Types.Repository\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\n\nmodule Reminder =\n\n    [<GenerateSerializer>]\n    type ReminderState =\n        | EmptyReminderState\n        | OwnerPhysicalDeletion of Owner.PhysicalDeletionReminderState\n        | OrganizationPhysicalDeletion of Organization.PhysicalDeletionReminderState\n        | RepositoryPhysicalDeletion of Repository.PhysicalDeletionReminderState\n        | BranchPhysicalDeletion of Branch.PhysicalDeletionReminderState\n        | ReferencePhysicalDeletion of Reference.PhysicalDeletionReminderState\n        | DirectoryVersionPhysicalDeletion of DirectoryVersion.PhysicalDeletionReminderState\n        | DirectoryVersionDeleteCachedState of DirectoryVersion.PhysicalDeletionReminderState\n        | DirectoryVersionDeleteZipFile of DirectoryVersion.PhysicalDeletionReminderState\n        | DiffDeleteCachedState of Diff.DeleteCachedStateReminderState\n\n    /// Defines all reminders used in Grace.\n    type ReminderDto =\n        {\n            Class: string\n            ReminderId: ReminderId\n            ActorName: string\n            ActorId: string\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            ReminderType: ReminderTypes\n            CreatedAt: Instant\n            ReminderTime: Instant\n            CorrelationId: CorrelationId\n            State: ReminderState\n        }\n\n        static member Default =\n            {\n                Class = nameof ReminderDto\n                ReminderId = ReminderId.Empty\n                ActorName = String.Empty\n                ActorId = String.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                ReminderType = ReminderTypes.Maintenance // This is the default because something has to be; there's no significance to it.\n                CreatedAt = Instant.MinValue\n                ReminderTime = Instant.MinValue\n                CorrelationId = String.Empty\n                State = ReminderState.EmptyReminderState\n            }\n\n        /// Creates a ReminderDto.\n        static member Create actorName actorId ownerId organizationId repositoryId reminderType reminderTime state correlationId =\n            {\n                Class = nameof ReminderDto\n                ReminderId = ReminderId.NewGuid()\n                ActorName = actorName\n                ActorId = actorId\n                OwnerId = ownerId\n                OrganizationId = organizationId\n                RepositoryId = repositoryId\n                ReminderType = reminderType\n                CreatedAt = getCurrentInstant ()\n                ReminderTime = reminderTime\n                CorrelationId = correlationId\n                State = state\n            }\n\n        override this.ToString() = serialize this\n\n    type ReminderWrapper() =\n        member val public Reminder: ReminderDto = ReminderDto.Default with get, set\n        override this.ToString() = serialize this\n"
  },
  {
    "path": "src/Grace.Types/Repository.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Repository =\n\n    /// The state held in the database when creating a physical deletion reminder for a repository.\n    type PhysicalDeletionReminderState = { DeleteReason: DeleteReason; CorrelationId: CorrelationId }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type RepositoryCommand =\n        | Create of\n            repositoryName: RepositoryName *\n            repositoryId: RepositoryId *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            objectStorageProvider: ObjectStorageProvider\n        | Initialize\n        | SetObjectStorageProvider of objectStorageProvider: ObjectStorageProvider\n        | SetStorageAccountName of storageAccountName: StorageAccountName\n        | SetStorageContainerName of storageContainerName: StorageContainerName\n        | SetRepositoryType of repositoryVisibility: RepositoryType\n        | SetRepositoryStatus of repositoryStatus: RepositoryStatus\n        | SetRecordSaves of recordSaves: bool\n        | SetAllowsLargeFiles of allowsLargeFiles: bool\n        | SetAnonymousAccess of anonymousAccess: bool\n        | SetDefaultServerApiVersion of defaultServerApiVersion: string\n        | SetDefaultBranchName of defaultBranchName: BranchName\n        | SetLogicalDeleteDays of duration: single\n        | SetSaveDays of duration: single\n        | SetCheckpointDays of duration: single\n        | SetDirectoryVersionCacheDays of duration: single\n        | SetDiffCacheDays of duration: single\n        | SetName of repositoryName: RepositoryName\n        | SetDescription of description: string\n        | SetConflictResolutionPolicy of conflictResolutionPolicy: ConflictResolutionPolicy\n        | DeleteLogical of force: bool * DeleteReason: DeleteReason\n        | DeletePhysical\n        | Undelete\n\n        static member GetKnownTypes() = GetKnownTypes<RepositoryCommand>()\n\n    /// Defines the events for the Repository actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type RepositoryEventType =\n        | Created of\n            repositoryName: RepositoryName *\n            repositoryId: RepositoryId *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            objectStorageProvider: ObjectStorageProvider\n        | Initialized\n        | ObjectStorageProviderSet of objectStorageProvider: ObjectStorageProvider\n        | StorageAccountNameSet of storageAccountName: StorageAccountName\n        | StorageContainerNameSet of storageContainerName: StorageContainerName\n        | RepositoryTypeSet of repositoryVisibility: RepositoryType\n        | RepositoryStatusSet of repositoryStatus: RepositoryStatus\n        | AllowsLargeFilesSet of allowsLargeFiles: bool\n        | AnonymousAccessSet of anonymousAccess: bool\n        | RecordSavesSet of recordSaves: bool\n        | DefaultServerApiVersionSet of defaultServerApiVersion: string\n        | DefaultBranchNameSet of defaultBranchName: BranchName\n        | LogicalDeleteDaysSet of duration: single\n        | SaveDaysSet of duration: single\n        | CheckpointDaysSet of duration: single\n        | DirectoryVersionCacheDaysSet of duration: single\n        | DiffCacheDaysSet of duration: single\n        | NameSet of repositoryName: RepositoryName\n        | DescriptionSet of description: string\n        | ConflictResolutionPolicySet of conflictResolutionPolicy: ConflictResolutionPolicy\n        | LogicalDeleted of force: bool * DeleteReason: DeleteReason\n        | PhysicalDeleted\n        | Undeleted\n\n        static member GetKnownTypes() = GetKnownTypes<RepositoryEventType>()\n\n    /// Record that holds the event type and metadata for a Repository event.\n    type RepositoryEvent =\n        {\n            /// The RepositoryEventType case that describes the event.\n            Event: RepositoryEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// The RepositoryDto is a data transfer object that represents a repository in the system.\n    type RepositoryDto =\n        {\n            Class: string\n            RepositoryId: RepositoryId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryName: RepositoryName\n            ObjectStorageProvider: ObjectStorageProvider\n            StorageAccountName: StorageAccountName\n            StorageContainerName: StorageContainerName\n            RepositoryType: RepositoryType\n            RepositoryStatus: RepositoryStatus\n            AnonymousAccess: bool\n            AllowsLargeFiles: bool\n            DefaultServerApiVersion: string\n            DefaultBranchName: BranchName\n            LogicalDeleteDays: single\n            SaveDays: single\n            CheckpointDays: single\n            DirectoryVersionCacheDays: single\n            DiffCacheDays: single\n            Description: string\n            RecordSaves: bool\n            ConflictResolutionPolicy: ConflictResolutionPolicy\n            CreatedAt: Instant\n            InitializedAt: Instant option\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        static member Default =\n            {\n                Class = nameof RepositoryDto\n                RepositoryId = Guid.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryName = RepositoryName String.Empty\n                ObjectStorageProvider = ObjectStorageProvider.Unknown\n                StorageAccountName = String.Empty\n                StorageContainerName = \"grace-objects\"\n                RepositoryType = RepositoryType.Private\n                RepositoryStatus = RepositoryStatus.Active\n                AnonymousAccess = false\n                AllowsLargeFiles = false\n                DefaultServerApiVersion = \"latest\"\n                DefaultBranchName = BranchName Constants.InitialBranchName\n                LogicalDeleteDays = 30.0f\n                SaveDays = 7.0f\n                CheckpointDays = 365.0f\n                DirectoryVersionCacheDays = 1.0f\n                DiffCacheDays = 1.0f\n                Description = String.Empty\n                RecordSaves = true\n                ConflictResolutionPolicy = ConflictResolutionPolicy.ConflictsAllowed 0.8f\n                CreatedAt = Constants.DefaultTimestamp\n                InitializedAt = None\n                UpdatedAt = None\n                DeletedAt = None\n                DeleteReason = String.Empty\n            }\n\n        /// Updates a RepositoryDto based on the provided RepositoryEvent.\n        static member UpdateDto repositoryEvent currentRepositoryDto =\n            let newRepositoryDto =\n                match repositoryEvent.Event with\n                | Created (name, repositoryId, ownerId, organizationId, objectStorageProvider) ->\n                    { RepositoryDto.Default with\n                        RepositoryName = name\n                        RepositoryId = repositoryId\n                        OwnerId = ownerId\n                        OrganizationId = organizationId\n                        ObjectStorageProvider = objectStorageProvider\n                        StorageAccountName = DefaultObjectStorageAccount\n                        StorageContainerName = $\"{repositoryId}\"\n                        CreatedAt = repositoryEvent.Metadata.Timestamp\n                    }\n                | Initialized -> { currentRepositoryDto with InitializedAt = Some(getCurrentInstant ()) }\n                | ObjectStorageProviderSet objectStorageProvider -> { currentRepositoryDto with ObjectStorageProvider = objectStorageProvider }\n                | StorageAccountNameSet storageAccountName -> { currentRepositoryDto with StorageAccountName = storageAccountName }\n                | StorageContainerNameSet containerName -> { currentRepositoryDto with StorageContainerName = containerName }\n                | RepositoryStatusSet repositoryStatus -> { currentRepositoryDto with RepositoryStatus = repositoryStatus }\n                | RepositoryTypeSet repositoryType -> { currentRepositoryDto with RepositoryType = repositoryType }\n                | RecordSavesSet recordSaves -> { currentRepositoryDto with RecordSaves = recordSaves }\n                | DefaultServerApiVersionSet version -> { currentRepositoryDto with DefaultServerApiVersion = version }\n                | DefaultBranchNameSet defaultBranchName -> { currentRepositoryDto with DefaultBranchName = defaultBranchName }\n                | LogicalDeleteDaysSet days -> { currentRepositoryDto with LogicalDeleteDays = days }\n                | SaveDaysSet days -> { currentRepositoryDto with SaveDays = days }\n                | CheckpointDaysSet days -> { currentRepositoryDto with CheckpointDays = days }\n                | DirectoryVersionCacheDaysSet days -> { currentRepositoryDto with DirectoryVersionCacheDays = days }\n                | DiffCacheDaysSet days -> { currentRepositoryDto with DiffCacheDays = days }\n                | NameSet repositoryName -> { currentRepositoryDto with RepositoryName = repositoryName }\n                | DescriptionSet description -> { currentRepositoryDto with Description = description }\n                | ConflictResolutionPolicySet policy -> { currentRepositoryDto with ConflictResolutionPolicy = policy }\n                | LogicalDeleted _ -> { currentRepositoryDto with DeletedAt = Some(getCurrentInstant ()) }\n                | PhysicalDeleted -> currentRepositoryDto // Do nothing because it's about to be deleted anyway.\n                | Undeleted -> { currentRepositoryDto with DeletedAt = None; DeleteReason = String.Empty }\n                | AllowsLargeFilesSet allowsLargeFiles -> { currentRepositoryDto with AllowsLargeFiles = allowsLargeFiles }\n                | AnonymousAccessSet anonymousAccess -> { currentRepositoryDto with AnonymousAccess = anonymousAccess }\n\n            { newRepositoryDto with UpdatedAt = Some repositoryEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/RequiredAction.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Collections.Generic\nopen System.Runtime.Serialization\n\nmodule RequiredAction =\n    /// The taxonomy for required actions.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type RequiredActionType =\n        | AcknowledgePolicyChange\n        | ProvideHumanApproval\n        | ResolveFinding\n        | RunValidation\n        | FixValidationFailure\n        | ResolveConflict\n        | ReAckDueToBaselineDrift\n        | ProvideMigrationNotes\n\n        static member GetKnownTypes() = GetKnownTypes<RequiredActionType>()\n\n    /// Machine-readable required action description.\n    [<GenerateSerializer>]\n    type RequiredActionDto =\n        {\n            RequiredActionType: RequiredActionType\n            TargetId: string option\n            Reason: string\n            Parameters: Dictionary<string, string>\n            SuggestedCliCommand: string option\n            SuggestedApiCall: string option\n        }\n"
  },
  {
    "path": "src/Grace.Types/Review.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Policy\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Review =\n    /// Defines the change type for a modified path.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type PathChangeType =\n        | Added\n        | Modified\n        | Deleted\n        | Renamed\n\n        static member GetKnownTypes() = GetKnownTypes<PathChangeType>()\n\n    /// Represents a modified path in a deterministic analysis.\n    [<GenerateSerializer>]\n    type PathChange = { RelativePath: RelativePath; ChangeType: PathChangeType }\n\n    /// Aggregated churn metrics for a delta.\n    [<GenerateSerializer>]\n    type ChurnMetrics = { LinesAdded: int; LinesRemoved: int; FilesChanged: int; RenamedCount: int }\n\n    /// Indicates whether test evidence is available.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type TestEvidencePresence =\n        | Unknown\n        | Present\n        | Missing\n\n        static member GetKnownTypes() = GetKnownTypes<TestEvidencePresence>()\n\n    /// Volatility signal representing reference creation activity.\n    [<GenerateSerializer>]\n    type VolatilitySignal = { ReferencesCreated: int; WindowDays: int }\n\n    /// Deterministic triggers that can be fired by deterministic review analysis.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type DeterministicTrigger =\n        | ChurnLinesExceeded\n        | SensitivePathTouched\n        | DependencyConfigChanged\n        | ApiSurfaceSignalDetected\n        | FileRewriteStorm\n        | RenameStorm\n        | GeneratedFilesChanged\n        | BinaryFilesChanged\n        | HighVolatility\n\n        static member GetKnownTypes() = GetKnownTypes<DeterministicTrigger>()\n\n    /// Deterministic analysis output.\n    [<GenerateSerializer>]\n    type DeterministicRiskProfile =\n        {\n            ReferenceId: ReferenceId\n            PolicySnapshotId: PolicySnapshotId\n            ChangedPaths: PathChange list\n            Churn: ChurnMetrics\n            SensitivePathsTouched: RelativePath list\n            DependencyConfigChanges: RelativePath list\n            ApiSurfaceSignals: RelativePath list\n            TestEvidence: TestEvidencePresence\n            GeneratedFiles: RelativePath list\n            BinaryFiles: RelativePath list\n            Volatility: VolatilitySignal option\n            TriggersFired: DeterministicTrigger list\n            IsNonTrivialSignal: bool\n            NonTrivialTriggers: DeterministicTrigger list\n            CreatedAt: Instant\n        }\n\n        static member Default =\n            {\n                ReferenceId = ReferenceId.Empty\n                PolicySnapshotId = PolicySnapshotId String.Empty\n                ChangedPaths = []\n                Churn = { LinesAdded = 0; LinesRemoved = 0; FilesChanged = 0; RenamedCount = 0 }\n                SensitivePathsTouched = []\n                DependencyConfigChanges = []\n                ApiSurfaceSignals = []\n                TestEvidence = TestEvidencePresence.Unknown\n                GeneratedFiles = []\n                BinaryFiles = []\n                Volatility = None\n                TriggersFired = []\n                IsNonTrivialSignal = false\n                NonTrivialTriggers = []\n                CreatedAt = Constants.DefaultTimestamp\n            }\n\n    /// Identifies the stage for an evidence set.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type EvidenceStage =\n        | Triage\n        | Deep\n\n        static member GetKnownTypes() = GetKnownTypes<EvidenceStage>()\n\n    /// Budget limits for evidence selection.\n    [<GenerateSerializer>]\n    type EvidenceBudget = { MaxFiles: int; MaxHunksPerFile: int; MaxLinesPerHunk: int; MaxTotalBytes: int; MaxTokens: int }\n\n    /// A selected evidence slice.\n    [<GenerateSerializer>]\n    type EvidenceSlice = { RelativePath: RelativePath; StartLine: int; EndLine: int; Content: string; IsRedacted: bool }\n\n    /// Reason for evidence selection.\n    [<GenerateSerializer>]\n    type EvidenceScoreReason = { Feature: string; Score: float }\n\n    /// Summary of a selected evidence slice.\n    [<GenerateSerializer>]\n    type EvidenceSliceSummary = { RelativePath: RelativePath; StartLine: int; EndLine: int; Score: float; Reasons: EvidenceScoreReason list }\n\n    /// Full evidence set for model input.\n    [<GenerateSerializer>]\n    type EvidenceSet = { Stage: EvidenceStage; Slices: EvidenceSlice list; Budget: EvidenceBudget; TotalBytes: int; EstimatedTokens: int }\n\n    /// Summary of evidence selection for auditability.\n    [<GenerateSerializer>]\n    type EvidenceSetSummary =\n        {\n            Stage: EvidenceStage\n            SelectedFiles: RelativePath list\n            SliceSummaries: EvidenceSliceSummary list\n            Budget: EvidenceBudget\n            TotalBytes: int\n            EstimatedTokens: int\n            TopReasons: EvidenceScoreReason list\n        }\n\n    /// Receipt describing a model analysis run.\n    [<GenerateSerializer>]\n    type AnalysisReceipt =\n        {\n            AnalysisReceiptId: AnalysisReceiptId\n            Stage: EvidenceStage\n            PolicySnapshotId: PolicySnapshotId\n            EvidenceHash: Sha256Hash\n            EvidenceSummary: EvidenceSetSummary\n            ModelId: string\n            MaxTokens: int\n            OutputHash: Sha256Hash\n            TriggerReasons: string list\n            CreatedAt: Instant\n            Principal: UserId\n        }\n\n        static member Default =\n            {\n                AnalysisReceiptId = Guid.Empty\n                Stage = EvidenceStage.Triage\n                PolicySnapshotId = PolicySnapshotId String.Empty\n                EvidenceHash = Sha256Hash String.Empty\n                EvidenceSummary =\n                    {\n                        Stage = EvidenceStage.Triage\n                        SelectedFiles = []\n                        SliceSummaries = []\n                        Budget = { MaxFiles = 0; MaxHunksPerFile = 0; MaxLinesPerHunk = 0; MaxTotalBytes = 0; MaxTokens = 0 }\n                        TotalBytes = 0\n                        EstimatedTokens = 0\n                        TopReasons = []\n                    }\n                ModelId = String.Empty\n                MaxTokens = 0\n                OutputHash = Sha256Hash String.Empty\n                TriggerReasons = []\n                CreatedAt = Constants.DefaultTimestamp\n                Principal = UserId String.Empty\n            }\n\n    /// The Id of a deterministic chapter.\n    type ChapterId = Sha256Hash\n\n    /// The Id of a review finding.\n    type FindingId = Guid\n\n    /// The Id of an analysis receipt.\n    type AnalysisReceiptId = Guid\n\n    /// Severity levels for review findings.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type FindingSeverity =\n        | Info\n        | Low\n        | Medium\n        | High\n        | Critical\n\n        static member GetKnownTypes() = GetKnownTypes<FindingSeverity>()\n\n    /// Categories for review findings.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type FindingCategory =\n        | Security\n        | Performance\n        | Api\n        | Config\n        | Tests\n        | Behavior\n        | Other of string\n\n        static member GetKnownTypes() = GetKnownTypes<FindingCategory>()\n\n    /// Resolution state for a finding.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type FindingResolutionState =\n        | Open\n        | Approved\n        | Rejected\n        | NeedsChanges\n        | Deferred\n        | Superseded\n\n        static member GetKnownTypes() = GetKnownTypes<FindingResolutionState>()\n\n    /// Evidence reference for a finding.\n    [<GenerateSerializer>]\n    type EvidenceReference = { RelativePath: RelativePath; StartLine: int; EndLine: int }\n\n    /// Record describing a review finding.\n    [<GenerateSerializer>]\n    type Finding =\n        {\n            FindingId: FindingId\n            Severity: FindingSeverity\n            Category: FindingCategory\n            Description: string\n            Rationale: string\n            RequiredActionType: string\n            EvidenceReferences: EvidenceReference list\n            ResolutionState: FindingResolutionState\n            ResolvedBy: UserId option\n            ResolvedAt: Instant option\n            ResolutionNote: string option\n        }\n\n    /// Deterministic chapter representation.\n    [<GenerateSerializer>]\n    type Chapter =\n        {\n            ChapterId: ChapterId\n            Title: string\n            Summary: string\n            Paths: RelativePath list\n            FindingIds: FindingId list\n            Evidence: EvidenceSliceSummary list\n        }\n\n    /// Review checkpoint for incremental review tracking.\n    [<GenerateSerializer>]\n    type ReviewCheckpoint =\n        {\n            ReviewCheckpointId: ReviewCheckpointId\n            PromotionSetId: PromotionSetId option\n            ReviewedUpToReferenceId: ReferenceId\n            PolicySnapshotId: PolicySnapshotId\n            Reviewer: UserId\n            Timestamp: Instant\n        }\n\n    /// Summary of validation results included in the review notes.\n    [<GenerateSerializer>]\n    type ValidationSummary = { ValidationResultIds: ValidationResultId list; Summary: string }\n\n    /// Primary review notes for a promotion set.\n    [<GenerateSerializer>]\n    type ReviewNotes =\n        {\n            Class: string\n            ReviewNotesId: ReviewNotesId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            PromotionSetId: PromotionSetId option\n            PolicySnapshotId: PolicySnapshotId\n            Summary: string\n            Chapters: Chapter list\n            Findings: Finding list\n            ImpactMap: string\n            EvidenceSetSummary: EvidenceSetSummary option\n            ValidationSummary: ValidationSummary option\n            EscalationReceiptIds: AnalysisReceiptId list\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n        }\n\n        static member Default =\n            {\n                Class = nameof ReviewNotes\n                ReviewNotesId = ReviewNotesId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                PromotionSetId = None\n                PolicySnapshotId = PolicySnapshotId String.Empty\n                Summary = String.Empty\n                Chapters = []\n                Findings = []\n                ImpactMap = String.Empty\n                EvidenceSetSummary = None\n                ValidationSummary = None\n                EscalationReceiptIds = []\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n            }\n\n    /// Defines the commands for the Review actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type ReviewCommand =\n        | UpsertNotes of reviewNotes: ReviewNotes\n        | ResolveFinding of findingId: FindingId * resolutionState: FindingResolutionState * resolvedBy: UserId * note: string option\n        | AddCheckpoint of checkpoint: ReviewCheckpoint\n\n        static member GetKnownTypes() = GetKnownTypes<ReviewCommand>()\n\n    /// Defines the events for the Review actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type ReviewEventType =\n        | NotesUpserted of reviewNotes: ReviewNotes\n        | FindingResolved of findingId: FindingId * resolutionState: FindingResolutionState * resolvedBy: UserId * note: string option\n        | CheckpointAdded of checkpoint: ReviewCheckpoint\n\n        static member GetKnownTypes() = GetKnownTypes<ReviewEventType>()\n\n    /// Record that holds the event type and metadata for a Review event.\n    type ReviewEvent =\n        {\n            /// The ReviewEventType case that describes the event.\n            Event: ReviewEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n"
  },
  {
    "path": "src/Grace.Types/Types.Types.fs",
    "content": "namespace Grace.Types\n\nopen DiffPlex.DiffBuilder.Model\nopen Grace.Shared.Constants\nopen Grace.Shared.Utilities\nopen NodaTime\nopen Orleans\nopen System\nopen System.Collections.Concurrent\nopen System.Collections.Generic\nopen System.IO\nopen System.Linq\nopen System.Runtime.Serialization\nopen System.Text\nopen System.Threading.Tasks\nopen System.Text.Json\nopen System.IO.Enumeration\nopen Microsoft.Extensions.ObjectPool\nopen System.Text.Json.Serialization\nopen MessagePack\n\nmodule Types =\n\n    // Domain nouns\n    /// The Id of the branch.\n    type BranchId = Guid\n\n    /// The name of the branch.\n    type BranchName = string\n\n    /// The name of the storage container used by Grace.\n    type ContainerName = string\n\n    /// The CorrelationId used during a Grace operation.\n    type CorrelationId = string\n\n    /// The reason given for deleting a branch or reference.\n    type DeleteReason = string\n\n    /// The Id of the directory version.\n    type DirectoryVersionId = Guid\n\n    /// The file path of a file in a repository.\n    type FilePath = string\n\n    /// An entry in the .graceignore file.\n    type GraceIgnoreEntry = string\n\n    /// The Id of the organization.\n    type OrganizationId = Guid\n\n    /// The name of the organization.\n    type OrganizationName = string\n\n    /// The Id of the owner of the repository.\n    type OwnerId = Guid\n\n    /// The name of the owner of the repository.\n    type OwnerName = string\n\n    /// The Id of the parent branch.\n    type ParentBranchId = BranchId\n\n    /// The Id of the reference.\n    type ReferenceId = Guid\n\n    /// The Id of the work item.\n    type WorkItemId = Guid\n\n    /// The repository-scoped monotonically increasing work item number.\n    type WorkItemNumber = int64\n\n    /// The Id of the promotion set.\n    type PromotionSetId = Guid\n\n    /// The Id of a promotion set step.\n    type PromotionSetStepId = Guid\n\n    /// The Id of the review notes payload.\n    type ReviewNotesId = Guid\n\n    /// The Id of the review checkpoint.\n    type ReviewCheckpointId = Guid\n\n    /// The Id of an analysis receipt.\n    type AnalysisReceiptId = Guid\n\n    /// The Id of a conflict resolution receipt.\n    type ConflictReceiptId = Guid\n\n    /// The Id of a validation set.\n    type ValidationSetId = Guid\n\n    /// The Id of a validation result.\n    type ValidationResultId = Guid\n\n    /// The Id of an artifact.\n    type ArtifactId = Guid\n\n    /// The text of the reference, generally submitted as the -m parameter in `grace save/checkpoint/commit/etc.`.\n    type ReferenceText = string\n\n    /// The Id of the reminder.\n    type ReminderId = Guid\n\n    /// The Id of the repository.\n    type RepositoryId = Guid\n\n    /// The name of the repository.\n    type RepositoryName = string\n\n    /// The path from the root directory of the repository.\n    ///\n    /// Example: Repository root directory is \"C:\\Source\\Grace\",\n    /// the full file path is \"C:\\Source\\Grace\\src\\Grace.Shared\\Types.Shared.fs\",\n    /// and the relative path is \"src\\Grace.Shared\\Types.Shared.fs\".\n    type RelativePath = string\n\n    /// A SHA-256 hash value.\n    type Sha256Hash = string\n\n    type StorageAccountName = string\n    type StorageConnectionString = string\n    type StorageContainerName = string\n\n    /// A Uri for a file in object storage that has a shared access signature (SAS) token.\n    type UriWithSharedAccessSignature = Uri\n\n    type UserId = string\n\n    /// The result of a single validation check.\n    type ValidationResult<'T> = ValueTask<Result<unit, 'T>>\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type LineEndings =\n        | CrLf\n        | Cr\n        | PlatformDependent\n\n        static member GetKnownTypes() = GetKnownTypes<LineEndings>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ObjectStorageProvider =\n        | AWSS3\n        | AzureBlobStorage\n        | GoogleCloudStorage\n        | Unknown // Not calling it None because that really belongs to Option.\n\n        static member DefaultObjectStorageProvider = ObjectStorageProvider.AzureBlobStorage\n\n        static member GetKnownTypes() = GetKnownTypes<ObjectStorageProvider>()\n\n    /// Indicates what database is being used for Actor state storage.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ActorStateStorageProvider =\n        | AzureCosmosDb\n        | MongoDB\n        | Unknown // Not calling it None because that really belongs to Option.\n        // etc.\n        static member GetKnownTypes() = GetKnownTypes<ActorStateStorageProvider>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type OwnerType =\n        | Public\n        | Private\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n        static member GetKnownTypes() = GetKnownTypes<OwnerType>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type OrganizationType =\n        | Public\n        | Private\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n        static member GetKnownTypes() = GetKnownTypes<OrganizationType>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type SearchVisibility =\n        | Visible\n        | NotVisible\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n        static member GetKnownTypes() = GetKnownTypes<SearchVisibility>()\n\n    /// Defines the different types of references that can exist in a branch.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ReferenceType =\n        | Promotion\n        | Commit\n        | Checkpoint\n        | Save\n        | Tag\n        | External\n        | Rebase\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n        static member FromString s = discriminatedUnionFromString<ReferenceType> s\n\n        static member GetKnownTypes() = GetKnownTypes<ReferenceType>()\n\n    // Records\n\n    /// EventMetadata is included in the recording of every event that occurs in Grace.\n    [<CLIMutable>]\n    type EventMetadata =\n        {\n            Timestamp: Instant\n            CorrelationId: CorrelationId\n            Principal: string\n            Properties: Dictionary<string, string>\n        }\n\n        override this.ToString() = serialize this\n\n        static member New correlationId principal =\n            { Timestamp = getCurrentInstant (); CorrelationId = correlationId; Principal = principal; Properties = Dictionary<string, string>() }\n\n    /// A FileVersion represents a version of a file in a repository with unique contents, and therefore with a unique SHA-256 hash. It is immutable.\n    ///\n    /// It is the server-side representation of the LocalFileVersion type, used for the local object cache.\n    [<CLIMutable; MessagePackObject; GenerateSerializer>]\n    type FileVersion =\n        {\n            [<Key(0)>]\n            Class: string\n            //RepositoryId: RepositoryId\n            [<Key(1)>]\n            RelativePath: RelativePath\n            [<Key(2)>]\n            Sha256Hash: Sha256Hash\n            [<Key(3)>]\n            IsBinary: bool\n            [<Key(4)>]\n            Size: int64\n            [<Key(5)>]\n            CreatedAt: Instant\n            [<Key(6)>]\n            BlobUri: string\n        }\n\n        static member Create\n            //(repositoryId: RepositoryId)\n            (relativePath: RelativePath)\n            (sha256Hash: Sha256Hash)\n            (blobUri: string)\n            (isBinary: bool)\n            (size: int64)\n            =\n            {\n                Class = \"FileVersion\"\n                //RepositoryId = repositoryId\n                RelativePath = RelativePath(normalizeFilePath $\"{relativePath}\")\n                Sha256Hash = sha256Hash\n                BlobUri = blobUri\n                IsBinary = isBinary\n                Size = size\n                CreatedAt = getCurrentInstant ()\n            }\n\n        static member Default = FileVersion.Create String.Empty String.Empty String.Empty false 0L\n\n        /// Converts a FileVersion to a LocalFileVersion.\n        member this.ToLocalFileVersion lastWriteTimeUtc =\n            LocalFileVersion.Create this.RelativePath this.Sha256Hash this.IsBinary this.Size this.CreatedAt true lastWriteTimeUtc\n\n        /// Get the object directory file name, which includes the SHA256 Hash value. Example: hello.js -> hello_04bef0a4b298de9c02930234.js\n        [<IgnoreMember>]\n        member this.GetObjectFileName = getObjectFileName this.RelativePath this.Sha256Hash\n\n        /// Gets the relative directory path of the file. Example: \"/dir/subdir/file.js\" -> \"/dir/subdir/\".\n        [<IgnoreMember>]\n        member this.RelativeDirectory = getRelativeDirectory $\"{this.RelativePath}\" \"\"\n\n    /// A LocalFileVersion represents a version of a file in a repository with unique contents, and therefore with a unique SHA-256 hash. It is immutable.\n    ///\n    /// It is the local representation of the FileVersion type, used on the server.\n    and [<CLIMutable; MessagePackObject>] LocalFileVersion =\n        {\n            [<Key(0)>]\n            Class: string\n            //[<Key(1)>]\n            //RepositoryId: RepositoryId\n            [<Key(1)>]\n            RelativePath: RelativePath\n            [<Key(2)>]\n            Sha256Hash: Sha256Hash\n            [<Key(3)>]\n            IsBinary: bool\n            [<Key(4)>]\n            Size: int64\n            [<Key(5)>]\n            CreatedAt: Instant\n            [<Key(6)>]\n            UploadedToObjectStorage: bool\n            [<Key(7)>]\n            LastWriteTimeUtc: DateTime\n        }\n\n        static member Create\n            //(repositoryId: RepositoryId)\n            (relativePath: RelativePath)\n            (sha256Hash: Sha256Hash)\n            (isBinary: bool)\n            (size: int64)\n            (createdAt: Instant)\n            (uploadedToObjectStorage: bool)\n            (lastWriteTimeUtc: DateTime)\n            =\n            {\n                Class = \"LocalFileVersion\"\n                //RepositoryId = repositoryId\n                RelativePath = RelativePath(normalizeFilePath $\"{relativePath}\")\n                Sha256Hash = sha256Hash\n                IsBinary = isBinary\n                Size = size\n                CreatedAt = createdAt\n                UploadedToObjectStorage = uploadedToObjectStorage\n                LastWriteTimeUtc = lastWriteTimeUtc\n            }\n\n        /// Converts a LocalFileVersion to a FileVersion. NOTE: at this point, we don't know the BlobUri.\n        [<IgnoreMember>]\n        member this.ToFileVersion = FileVersion.Create this.RelativePath this.Sha256Hash String.Empty this.IsBinary this.Size\n\n        /// Get the object directory file name, which includes the SHA256 Hash value. Example: hello.js -> hello_04bef0a4b298de9c02930234.js\n        [<IgnoreMember>]\n        member this.GetObjectFileName = getObjectFileName this.RelativePath this.Sha256Hash\n\n        /// Gets the relative directory path of the file. Example: \"/dir/subdir/file.js\" -> \"/dir/subdir/\".\n        [<IgnoreMember>]\n        member this.RelativeDirectory = getRelativeDirectory $\"{this.RelativePath}\" \"\"\n\n    /// A DirectoryVersion represents a version of a directory in a repository with unique contents, and therefore with a unique SHA-256 hash.\n    ///\n    /// It is the server-side representation of the LocalDirectoryVersion type. LocalDirectoryVersion is used for the local object cache.\n    [<CLIMutable; MessagePackObject; GenerateSerializer>]\n    type DirectoryVersion =\n        {\n            [<Key(0)>]\n            Class: string\n            [<Key(1)>]\n            DirectoryVersionId: DirectoryVersionId\n            [<Key(2)>]\n            OwnerId: OwnerId\n            [<Key(3)>]\n            OrganizationId: OrganizationId\n            [<Key(4)>]\n            RepositoryId: RepositoryId\n            [<Key(5)>]\n            RelativePath: RelativePath\n            [<Key(6)>]\n            Sha256Hash: Sha256Hash\n            [<Key(7)>]\n            Directories: List<DirectoryVersionId>\n            [<Key(8)>]\n            Files: List<FileVersion>\n            [<Key(9)>]\n            Size: int64\n            [<Key(10)>]\n            CreatedAt: Instant\n            [<Key(11)>]\n            HashesValidated: bool\n        }\n\n        static member GetKnownTypes() = GetKnownTypes<DirectoryVersion>()\n\n        static member Default =\n            {\n                Class = nameof DirectoryVersion\n                DirectoryVersionId = DirectoryVersionId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                RelativePath = RelativePath String.Empty\n                Sha256Hash = Sha256Hash String.Empty\n                Directories = List<DirectoryVersionId>()\n                Files = List<FileVersion>()\n                Size = InitialDirectorySize\n                CreatedAt = DefaultTimestamp\n                HashesValidated = false\n            }\n\n        static member Create\n            (directoryVersionId: DirectoryVersionId)\n            (ownerId: OwnerId)\n            (organizationId: OrganizationId)\n            (repositoryId: RepositoryId)\n            (relativePath: RelativePath)\n            (sha256Hash: Sha256Hash)\n            (directories: List<DirectoryVersionId>)\n            (files: List<FileVersion>)\n            (size: int64)\n            =\n            {\n                Class = nameof DirectoryVersion\n                DirectoryVersionId = directoryVersionId\n                OwnerId = ownerId\n                OrganizationId = organizationId\n                RepositoryId = repositoryId\n                RelativePath = relativePath\n                Sha256Hash = sha256Hash\n                Directories = directories\n                Files = files\n                Size = size\n                CreatedAt = getCurrentInstant ()\n                HashesValidated = false\n            }\n\n        member this.ToLocalDirectoryVersion lastWriteTimeUtc =\n            LocalDirectoryVersion.Create\n                this.DirectoryVersionId\n                this.OwnerId\n                this.OrganizationId\n                this.RepositoryId\n                this.RelativePath\n                this.Sha256Hash\n                this.Directories\n                (this\n                    .Files\n                    .Select(fun f -> f.ToLocalFileVersion lastWriteTimeUtc)\n                    .ToList())\n                this.Size\n                lastWriteTimeUtc\n\n    /// A LocalDirectoryVersion represents a version of a directory in a repository with unique contents, and therefore with a unique SHA-256 hash.\n    ///\n    /// It is the local representation of the DirectoryVersion type. DirectoryVersion is used on the server.\n    and [<CLIMutable; MessagePackObject>] LocalDirectoryVersion =\n        {\n            [<Key(0)>]\n            Class: string\n            [<Key(1)>]\n            DirectoryVersionId: DirectoryVersionId\n            [<Key(2)>]\n            OwnerId: OwnerId\n            [<Key(3)>]\n            OrganizationId: OrganizationId\n            [<Key(4)>]\n            RepositoryId: RepositoryId\n            [<Key(5)>]\n            RelativePath: RelativePath\n            [<Key(6)>]\n            Sha256Hash: Sha256Hash\n            [<Key(7)>]\n            Directories: List<DirectoryVersionId>\n            [<Key(8)>]\n            Files: List<LocalFileVersion>\n            [<Key(9)>]\n            Size: int64\n            [<Key(10)>]\n            CreatedAt: Instant\n            [<Key(11)>]\n            LastWriteTimeUtc: DateTime\n        }\n\n        static member Default =\n            {\n                Class = \"LocalDirectoryVersion\"\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                DirectoryVersionId = DirectoryVersionId.Empty\n                RelativePath = RelativePath String.Empty\n                Sha256Hash = Sha256Hash String.Empty\n                Directories = List<DirectoryVersionId>()\n                Files = List<LocalFileVersion>()\n                Size = InitialDirectorySize\n                CreatedAt = DefaultTimestamp\n                LastWriteTimeUtc = DateTime.UtcNow\n            }\n\n        static member Create\n            (directoryVersionId: DirectoryVersionId)\n            (ownerId: OwnerId)\n            (organizationId: OrganizationId)\n            (repositoryId: RepositoryId)\n            (relativePath: RelativePath)\n            (sha256Hash: Sha256Hash)\n            (directories: List<DirectoryVersionId>)\n            (files: List<LocalFileVersion>)\n            (size: int64)\n            (lastWriteTimeUtc: DateTime)\n            =\n            {\n                Class = \"LocalDirectoryVersion\"\n                DirectoryVersionId = directoryVersionId\n                OwnerId = ownerId\n                OrganizationId = organizationId\n                RepositoryId = repositoryId\n                RelativePath = relativePath\n                Sha256Hash = sha256Hash\n                Directories = directories\n                Files = files\n                Size = size\n                CreatedAt = getCurrentInstant ()\n                LastWriteTimeUtc = lastWriteTimeUtc\n            }\n\n        /// Converts a LocalDirectoryVersion to a DirectoryVersion.\n        [<IgnoreMember>]\n        member this.ToDirectoryVersion =\n            DirectoryVersion.Create\n                this.DirectoryVersionId\n                this.OwnerId\n                this.OrganizationId\n                this.RepositoryId\n                this.RelativePath\n                this.Sha256Hash\n                this.Directories\n                (this\n                    .Files\n                    .Select(fun f -> f.ToFileVersion)\n                    .ToList())\n                this.Size\n\n    /// Specifies whether a specific entry in a directory is a DirectoryVersion or a FileVersion.\n    and [<KnownType(\"GetKnownTypes\"); GenerateSerializer>] DirectoryEntry =\n        | Directory of DirectoryVersion\n        | File of FileVersion\n\n        static member GetKnownTypes() = GetKnownTypes<DirectoryEntry>()\n\n        /// Converts a DirectoryEntry to a LocalDirectoryEntry.\n        member this.ToLocalDirectoryEntry lastWriteTimeUtc =\n            match this with\n            | DirectoryEntry.Directory d -> LocalDirectory(d.ToLocalDirectoryVersion lastWriteTimeUtc)\n            | DirectoryEntry.File f -> LocalFile(f.ToLocalFileVersion lastWriteTimeUtc)\n\n    /// Specifies whether a specific entry in a local directory is a LocalDirectoryVersion or a LocalFileVersion.\n    and [<KnownType(\"GetKnownTypes\"); GenerateSerializer>] LocalDirectoryEntry =\n        | LocalDirectory of LocalDirectoryVersion\n        | LocalFile of LocalFileVersion\n\n        static member GetKnownTypes() = GetKnownTypes<LocalDirectoryEntry>()\n\n        /// Converts a LocalDirectoryEntry to a DirectoryEntry.\n        member this.ToDirectoryEntry: DirectoryEntry =\n            match this with\n            | LocalDirectory d -> DirectoryEntry.Directory d.ToDirectoryVersion\n            | LocalFile f -> DirectoryEntry.File f.ToFileVersion\n\n    /// Specifies whether a repository is public or private.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type RepositoryType =\n        | Private\n        | Public\n\n        static member GetKnownTypes() = GetKnownTypes<RepositoryType>()\n\n    /// Specifies the current operational status of a repository.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type RepositoryStatus =\n        | Active\n        | Suspended\n        | Closed\n        | Deleted\n\n        static member GetKnownTypes() = GetKnownTypes<RepositoryStatus>()\n\n    // /// Defines the type of the list of validations used in server endpoints.\n    // type Validations<'T, 'U> = 'T -> Result<bool, 'U> list\n\n    /// Defines the specific permissions that can be granted to a user or group on a directory.\n    [<KnownType(\"GetKnownTypes\")>]\n    type DirectoryPermission =\n        | FullControl\n        | Modify\n        | ListContents\n        | Read\n        | NoAccess\n        | NotSet\n\n        static member GetKnownTypes() = GetKnownTypes<DirectoryPermission>()\n\n    /// Defines the kinds of links that can exist between references.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ReferenceLinkType =\n        | BasedOn of ReferenceId\n        | IncludedInPromotionSet of PromotionSetId\n        | PromotionSetTerminal of PromotionSetId // Marks the final promotion in a promotion set.\n\n        static member GetKnownTypes() = GetKnownTypes<ReferenceLinkType>()\n\n    /// Defines the permissions granted to a group defined by a claim.\n    ///\n    /// This is combined with a RelativePath to define a PathPermission.\n    [<GenerateSerializer>]\n    type ClaimPermission =\n        {\n            [<Id 0u>]\n            Claim: string\n            [<Id 1u>]\n            DirectoryPermission: DirectoryPermission\n        }\n\n    /// Defines a set of claims and their permissions that should be applied to a relative path in the repository.\n    ///\n    /// NOTE: This type is being used only at the directory level for now, but I intend to implement it at the file level as well.\n    [<GenerateSerializer>]\n    type PathPermission =\n        {\n            [<Id 0u>]\n            Path: RelativePath\n            [<Id 1u>]\n            Permissions: List<ClaimPermission>\n        }\n\n    /// Cleans up extra backslashes (escape characters) and converts \\r\\n to Environment.NewLine.\n    let cleanJson (s: string) =\n        s\n            .Replace(\"\\\\\\\\\\\\\\\\\", @\"\\\")\n            .Replace(\"\\\\\\\\\", @\"\\\")\n            .Replace(@\"\\r\\n\", Environment.NewLine)\n\n    /// A serializable view of a .NET Exception\n    type ExceptionObject =\n        {\n            Message: string\n            StackTrace: string\n            InnerException: ExceptionObject option\n        }\n\n        /// Checks if the ExceptionObject is set to the default value.\n        member this.IsDefault() = this = ExceptionObject.Default\n\n        static member Default = { Message = String.Empty; StackTrace = String.Empty; InnerException = None }\n\n        /// Creates an ExceptionObject from a .NET Exception.\n        static member Create(ex: Exception) =\n            {\n                Message = ex.Message\n                StackTrace = if String.IsNullOrEmpty ex.StackTrace then String.Empty else ex.StackTrace\n                InnerException =\n                    if ex.InnerException <> null then\n                        Some(ExceptionObject.Create(ex.InnerException))\n                    else\n                        None\n            }\n\n    /// The primary type used in Grace to represent successful results.\n    type GraceReturnValue<'T> =\n        {\n            ReturnValue: 'T\n            EventTime: Instant\n            CorrelationId: string\n            Properties: Dictionary<string, obj>\n        }\n\n        static member CreateWithMetadata<'T> (returnValue: 'T) (correlationId: string) (properties: Dictionary<string, obj>) =\n            { ReturnValue = returnValue; EventTime = getCurrentInstant (); CorrelationId = correlationId; Properties = properties }\n\n        static member Create<'T> (returnValue: 'T) (correlationId: string) =\n            GraceReturnValue.CreateWithMetadata returnValue correlationId (Dictionary<string, obj>())\n\n        /// Adds a key-value pair to GraceReturnValue's Properties dictionary.\n        member this.enhance((key: string), (value: obj)) =\n            //logToConsole $\"In GraceReturnValue.enhance: Enhancing GraceReturnValue with key: {key}, value: {value}.\"\n\n            match String.IsNullOrEmpty(key), isNull (value) with\n            | false, false -> this.Properties[ key ] <- value\n            | false, true -> this.Properties[ key ] <- null\n            | true, _ -> ()\n\n            this\n\n        /// Adds a set of key-value pairs from a Dictionary to GraceReturnValue's Properties dictionary.\n        member this.enhance(dict: IReadOnlyDictionary<string, obj>) =\n            // logToConsole $\"In GraceReturnValue.enhance: isNull(dict): {isNull (dict)}.\"\n            // logToConsole $\"In GraceReturnValue.enhance: Enhancing GraceReturnValue with {dict.Count} properties.\"\n\n            dict\n            |> Seq.iter (fun kvp -> this.enhance (kvp.Key, kvp.Value) |> ignore)\n\n            this\n\n        override this.ToString() =\n            // Breaking out the Properties because Dictionary<> doesn't have a good ToString() method.\n            let output =\n                {|\n                    ReturnValue = this.ReturnValue\n                    EventTime = this.EventTime\n                    CorrelationId = this.CorrelationId\n                    Properties = this.Properties.Select(fun kvp -> {| Key = kvp.Key; Value = kvp.Value |})\n                |}\n            //logToConsole $\"GraceReturnValue: {serialize output}\"\n            serialize output\n\n    /// The primary type used in Grace to represent error results.\n    type GraceError =\n        {\n            Exception: ExceptionObject\n            Error: string\n            EventTime: Instant\n            CorrelationId: string\n            Properties: Dictionary<string, obj>\n        }\n\n        static member Default =\n            {\n                Exception = ExceptionObject.Default\n                Error = \"Empty error message\"\n                EventTime = getCurrentInstant ()\n                CorrelationId = String.Empty\n                Properties = new Dictionary<string, obj>()\n            }\n\n        static member Create (error: string) (correlationId: string) =\n            {\n                Exception = ExceptionObject.Default\n                Error = error\n                EventTime = getCurrentInstant ()\n                CorrelationId = correlationId\n                Properties = new Dictionary<string, obj>()\n            }\n\n        static member CreateWithException (ex: Exception) (error: string) (correlationId: string) =\n            {\n                Exception = ExceptionObject.Create(ex)\n                Error = error\n                EventTime = getCurrentInstant ()\n                CorrelationId = correlationId\n                Properties = new Dictionary<string, obj>()\n            }\n\n        static member CreateWithMetadata (ex: Exception) (error: string) (correlationId: string) (properties: Dictionary<string, obj>) =\n            { Exception = ExceptionObject.Create(ex); Error = error; EventTime = getCurrentInstant (); CorrelationId = correlationId; Properties = properties }\n\n        /// Adds a key-value pair to GraceError's Properties dictionary.\n        member this.enhance(key: string, value: obj) =\n            match String.IsNullOrEmpty(key), isNull (value) with\n            | false, false -> this.Properties[ key ] <- $\"{value}\"\n            | false, true -> this.Properties[ key ] <- null\n            | true, _ -> ()\n\n            this\n\n        /// Adds a set of key-value pairs from a Dictionary to GraceError's Properties dictionary.\n        member this.enhance(dict: IReadOnlyDictionary<string, obj>) =\n            dict\n            |> Seq.iter (fun kvp -> this.Properties[ kvp.Key ] <- $\"{kvp.Value}\")\n\n            this\n\n        override this.ToString() =\n            let sb = stringBuilderPool.Get()\n\n            try\n                let errorText =\n                    this.Properties\n                    |> Seq.fold (fun (state: StringBuilder) kvp -> state.AppendLine($\"  {kvp.Key}: {kvp.Value}; \")) sb\n\n                if sb.Length >= 2 then sb.Remove(sb.Length - 2, 2) |> ignore\n\n                let message = if this.Exception.IsDefault() then this.Error else (serialize this.Exception)\n\n                $\"Error: {message}{Environment.NewLine}EventTime: {formatInstantExtended this.EventTime}{Environment.NewLine}CorrelationId: {this.CorrelationId}{Environment.NewLine}Properties:{Environment.NewLine}{errorText.ToString()}{Environment.NewLine}\"\n            finally\n                stringBuilderPool.Return(sb)\n\n    /// The primary type used to represent Grace operations results.\n    type GraceResult<'T> = Result<GraceReturnValue<'T>, GraceError>\n\n    /// Specifies whether a file system entry is a directory or a file.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type FileSystemEntryType =\n        | Directory\n        | File\n\n        static member GetKnownTypes() = GetKnownTypes<FileSystemEntryType>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Specifies whether a change detected in a diff is an add, change, or delete.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type DifferenceType =\n        | Add\n        | Change\n        | Delete\n\n        static member GetKnownTypes() = GetKnownTypes<DifferenceType>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// A file system difference is a change detected (at a file level) in a diff. It specifies the type of change (add, change, or delete), the type of file system entry (directory or file), and the relative path of the entry.\n    [<GenerateSerializer>]\n    type FileSystemDifference =\n        {\n            DifferenceType: DifferenceType\n            FileSystemEntryType: FileSystemEntryType\n            RelativePath: RelativePath\n        }\n\n        static member Create differenceType fileSystemEntryType relativePath =\n            { DifferenceType = differenceType; FileSystemEntryType = fileSystemEntryType; RelativePath = relativePath }\n\n    [<GenerateSerializer>]\n    type UploadMetadata = { RelativePath: RelativePath; BlobUriWithSasToken: Uri; Sha256Hash: Sha256Hash }\n\n    /// GraceIndex is Grace's representation of the contents of the local working directory (in GraceStatus), or of the object cache (in GraceObjectCache).\n    [<GenerateSerializer>]\n    type GraceIndex = ConcurrentDictionary<DirectoryVersionId, LocalDirectoryVersion>\n\n    /// GraceStatus is a snapshot of the status that `grace watch` holds about the repository and branch while running.\n    ///\n    /// It is serialized and written by `grace watch` in the inter-process cache file that Grace CLI uses to know that `grace watch` is running.\n    /// It gets deserialized by Grace CLI, and is used to speed up the CLI by holding pre-computed results when running certain commands.\n    ///\n    /// If the interprocess cache file is missing or corrupt, Grace CLI assumes that `grace watch` is not running, and runs all commands from scratch.\n    [<MessagePackObject>]\n    type GraceStatus =\n        {\n            [<Key(0)>]\n            Index: GraceIndex\n            [<Key(1)>]\n            RootDirectoryId: DirectoryVersionId\n            [<Key(2)>]\n            RootDirectorySha256Hash: Sha256Hash\n            [<Key(3)>]\n            LastSuccessfulFileUpload: Instant\n            [<Key(4)>]\n            LastSuccessfulDirectoryVersionUpload: Instant\n        }\n\n        static member Default =\n            {\n                Index = GraceIndex()\n                RootDirectoryId = DirectoryVersionId.Empty\n                RootDirectorySha256Hash = Sha256Hash String.Empty\n                LastSuccessfulFileUpload = getCurrentInstant ()\n                LastSuccessfulDirectoryVersionUpload = getCurrentInstant ()\n            }\n\n    /// GraceObjectCache is a snapshot of the contents of the local object cache.\n    [<MessagePackObject>]\n    type GraceObjectCache =\n        {\n            [<Key(0)>]\n            Index: GraceIndex\n        }\n\n        static member Default = { Index = GraceIndex() }\n\n    /// Holds the results of a diff between two versions of a file.\n    [<GenerateSerializer>]\n    type FileDiff =\n        {\n            RelativePath: RelativePath\n            FileSha1: Sha256Hash\n            CreatedAt1: Instant\n            FileSha2: Sha256Hash\n            CreatedAt2: Instant\n            IsBinary: bool\n            InlineDiff: List<DiffPiece []>\n            SideBySideOld: List<DiffPiece []>\n            SideBySideNew: List<DiffPiece []>\n        }\n\n        static member Create\n            (relativePath: RelativePath)\n            (fileSha1: Sha256Hash)\n            (createdAt1: Instant)\n            (fileSha2: Sha256Hash)\n            (createdAt2: Instant)\n            (isBinary: bool)\n            (inlineDiff: List<DiffPiece []>)\n            (sideBySideOld: List<DiffPiece []>)\n            (sideBySideNew: List<DiffPiece []>)\n            =\n            {\n                RelativePath = relativePath\n                FileSha1 = fileSha1\n                CreatedAt1 = createdAt1\n                FileSha2 = fileSha2\n                CreatedAt2 = createdAt2\n                IsBinary = isBinary\n                InlineDiff = inlineDiff\n                SideBySideOld = sideBySideOld\n                SideBySideNew = sideBySideNew\n            }\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type PromotionType =\n        | SingleStep\n        | Complex\n\n        static member GetKnownTypes() = GetKnownTypes<PromotionType>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Defines how promotions are handled for a branch.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type BranchPromotionMode =\n        | IndividualOnly // Default behavior: promotions always applied individually.\n        | GroupOnly // Promotions to this branch must go through a promotion group.\n        | Hybrid // Promotions can be grouped by default, with an opt-out flag.\n\n        static member GetKnownTypes() = GetKnownTypes<BranchPromotionMode>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Defines the conflict resolution policy for a repository.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ConflictResolutionPolicy =\n        | NoConflicts of unit // Any conflict blocks recomputation/apply.\n        | ConflictsAllowed of float32 // Conflicts allowed if model confidence >= threshold (0.0 to 1.0).\n\n        static member GetKnownTypes() = GetKnownTypes<ConflictResolutionPolicy>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Holds the entity Id's involved in an API call. It's populated in ValidateIds.Middleware.fs.\n    type GraceIds =\n        {\n            OwnerId: OwnerId\n            OwnerIdString: string\n            OwnerName: OwnerName\n            OrganizationId: OrganizationId\n            OrganizationIdString: string\n            OrganizationName: OrganizationName\n            RepositoryId: RepositoryId\n            RepositoryIdString: string\n            RepositoryName: RepositoryName\n            BranchId: BranchId\n            BranchIdString: string\n            BranchName: BranchName\n            CorrelationId: string\n            HasOwner: bool\n            HasOrganization: bool\n            HasRepository: bool\n            HasBranch: bool\n        }\n\n        static member Default =\n            {\n                OwnerId = OwnerId.Empty\n                OwnerIdString = String.Empty\n                OwnerName = OwnerName.Empty\n                OrganizationId = OrganizationId.Empty\n                OrganizationIdString = String.Empty\n                OrganizationName = OrganizationName.Empty\n                RepositoryId = RepositoryId.Empty\n                RepositoryIdString = String.Empty\n                RepositoryName = RepositoryName.Empty\n                BranchId = BranchId.Empty\n                BranchIdString = String.Empty\n                BranchName = BranchName.Empty\n                CorrelationId = String.Empty\n                HasOwner = false\n                HasOrganization = false\n                HasRepository = false\n                HasBranch = false\n            }\n\n        override this.ToString() = serialize this\n\n    /// Defines the different types of reminders used in Grace.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ReminderTypes =\n        /// Maintenance reminders are used to remind the actor to perform maintenance on itself.\n        | Maintenance\n        /// Physical deletion reminders are used to remind the actor to delete itself after the LogicalDelete hold time has expired.\n        | PhysicalDeletion\n        /// DeleteCachedState reminders are used to remind the actor to delete its cached state after the time set in the repository has expired.\n        | DeleteCachedState\n        /// DeleteZipFile reminders are used to remind the DirectoryVersion actor to delete the directory version contents .zip file after the time set in the repository has expired.\n        | DeleteZipFile\n\n        static member GetKnownTypes() = GetKnownTypes<ReminderTypes>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Defines the different statuses of a reminder.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ReminderStatus =\n        /// The reminder is pending and has not yet been dispatched.\n        | Pending\n        /// The reminder has been dispatched to the target actor.\n        | Dispatched\n        /// The reminder failed to execute.\n        | Failed\n        /// The reminder has been cancelled.\n        | Cancelled\n\n        static member GetKnownTypes() = GetKnownTypes<ReminderStatus>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Defines the different statuses of a .zip file.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ZipFileStatus =\n        | NotCreated\n        | Creating\n        | Exists\n\n        static member GetKnownTypes() = GetKnownTypes<ReminderStatus>()\n\n    [<GenerateSerializer>]\n    type Appearance = { Root: DirectoryVersionId; Parent: DirectoryVersionId; Created: Instant }\n\n    /// A list of known Grace Server API version strings.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ServerApiVersions =\n        | ``V2022-02-01``\n        | Latest\n        | Edge\n\n        static member GetKnownTypes() = GetKnownTypes<ServerApiVersions>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Supported outbound pub-sub providers for Grace.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type GracePubSubSystem =\n        | UnknownPubSubProvider\n        | AzureEventHubs\n        | AzureServiceBus\n        | AwsSqs\n        | GoogleCloudPubSub\n\n        static member GetKnownTypes() = GetKnownTypes<GracePubSubSystem>()\n\n        override this.ToString() = getDiscriminatedUnionFullName this\n\n    /// Settings for Azure Service Bus pub-sub integration.\n    [<GenerateSerializer>]\n    type AzureServiceBusPubSubSettings =\n        {\n            ConnectionString: string\n            FullyQualifiedNamespace: string\n            TopicName: string\n            SubscriptionName: string\n            UseManagedIdentity: bool\n        }\n\n    /// Settings for Grace pub-sub integration.\n    [<GenerateSerializer>]\n    type GracePubSubSettings =\n        {\n            System: GracePubSubSystem\n            AzureServiceBus: AzureServiceBusPubSubSettings option\n        }\n\n        static member Empty: GracePubSubSettings = { System = GracePubSubSystem.UnknownPubSubProvider; AzureServiceBus = None }\n"
  },
  {
    "path": "src/Grace.Types/Validation.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Artifact\nopen Grace.Types.Automation\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule Validation =\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ValidationExecutionMode =\n        | Synchronous\n        | AsyncCallback\n\n        static member GetKnownTypes() = GetKnownTypes<ValidationExecutionMode>()\n\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type ValidationStatus =\n        | Pass\n        | Fail\n        | Block\n        | Skipped\n\n        static member GetKnownTypes() = GetKnownTypes<ValidationStatus>()\n\n    [<GenerateSerializer>]\n    type Validation = { Name: string; Version: string; ExecutionMode: ValidationExecutionMode; RequiredForApply: bool }\n\n    [<GenerateSerializer>]\n    type ValidationSetRule = { EventTypes: AutomationEventType list; BranchNameGlob: string }\n\n    [<GenerateSerializer>]\n    type ValidationSetDto =\n        {\n            Class: string\n            ValidationSetId: ValidationSetId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            TargetBranchId: BranchId\n            Rules: ValidationSetRule list\n            Validations: Validation list\n            OnBehalfOf: UserId list\n            CreatedBy: UserId\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n            DeletedAt: Instant option\n            DeleteReason: DeleteReason\n        }\n\n        static member Default =\n            {\n                Class = nameof ValidationSetDto\n                ValidationSetId = ValidationSetId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                TargetBranchId = BranchId.Empty\n                Rules = []\n                Validations = []\n                OnBehalfOf = []\n                CreatedBy = UserId String.Empty\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n                DeletedAt = None\n                DeleteReason = String.Empty\n            }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ValidationSetCommand =\n        | Create of validationSet: ValidationSetDto\n        | Update of validationSet: ValidationSetDto\n        | DeleteLogical of force: bool * deleteReason: DeleteReason\n\n        static member GetKnownTypes() = GetKnownTypes<ValidationSetCommand>()\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ValidationSetEventType =\n        | Created of validationSet: ValidationSetDto\n        | Updated of validationSet: ValidationSetDto\n        | LogicalDeleted of force: bool * deleteReason: DeleteReason\n\n        static member GetKnownTypes() = GetKnownTypes<ValidationSetEventType>()\n\n    type ValidationSetEvent = { Event: ValidationSetEventType; Metadata: EventMetadata }\n\n    [<GenerateSerializer>]\n    type ValidationOutput = { Status: ValidationStatus; Summary: string; ArtifactIds: ArtifactId list }\n\n    [<GenerateSerializer>]\n    type ValidationResultDto =\n        {\n            Class: string\n            ValidationResultId: ValidationResultId\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            ValidationSetId: ValidationSetId option\n            PromotionSetId: PromotionSetId option\n            PromotionSetStepId: PromotionSetStepId option\n            StepsComputationAttempt: int option\n            ValidationName: string\n            ValidationVersion: string\n            Output: ValidationOutput\n            OnBehalfOf: UserId list\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n        }\n\n        static member Default =\n            {\n                Class = nameof ValidationResultDto\n                ValidationResultId = ValidationResultId.Empty\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                ValidationSetId = None\n                PromotionSetId = None\n                PromotionSetStepId = None\n                StepsComputationAttempt = None\n                ValidationName = String.Empty\n                ValidationVersion = String.Empty\n                Output = { Status = ValidationStatus.Skipped; Summary = String.Empty; ArtifactIds = [] }\n                OnBehalfOf = []\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n            }\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ValidationResultCommand =\n        | Record of validationResult: ValidationResultDto\n\n        static member GetKnownTypes() = GetKnownTypes<ValidationResultCommand>()\n\n    [<KnownType(\"GetKnownTypes\")>]\n    type ValidationResultEventType =\n        | Recorded of validationResult: ValidationResultDto\n\n        static member GetKnownTypes() = GetKnownTypes<ValidationResultEventType>()\n\n    type ValidationResultEvent = { Event: ValidationResultEventType; Metadata: EventMetadata }\n\n    module ValidationSetDto =\n        let UpdateDto (validationSetEvent: ValidationSetEvent) (current: ValidationSetDto) =\n            let updated =\n                match validationSetEvent.Event with\n                | ValidationSetEventType.Created validationSet -> validationSet\n                | ValidationSetEventType.Updated validationSet -> validationSet\n                | ValidationSetEventType.LogicalDeleted (_, deleteReason) ->\n                    { current with DeletedAt = Some(getCurrentInstant ()); DeleteReason = deleteReason }\n\n            let onBehalfOf =\n                updated.OnBehalfOf\n                |> List.append [ UserId validationSetEvent.Metadata.Principal ]\n                |> List.distinct\n\n            { updated with OnBehalfOf = onBehalfOf; UpdatedAt = Some validationSetEvent.Metadata.Timestamp }\n\n    module ValidationResultDto =\n        let UpdateDto (validationResultEvent: ValidationResultEvent) (_current: ValidationResultDto) =\n            match validationResultEvent.Event with\n            | ValidationResultEventType.Recorded validationResult ->\n                let onBehalfOf =\n                    validationResult.OnBehalfOf\n                    |> List.append [ UserId validationResultEvent.Metadata.Principal ]\n                    |> List.distinct\n\n                { validationResult with OnBehalfOf = onBehalfOf; UpdatedAt = Some validationResultEvent.Metadata.Timestamp }\n"
  },
  {
    "path": "src/Grace.Types/WorkItem.Types.fs",
    "content": "namespace Grace.Types\n\nopen Grace.Shared\nopen Grace.Shared.Utilities\nopen Grace.Types.Types\nopen NodaTime\nopen Orleans\nopen System\nopen System.Runtime.Serialization\n\nmodule WorkItem =\n    /// Defines the status of a work item.\n    [<KnownType(\"GetKnownTypes\"); GenerateSerializer>]\n    type WorkItemStatus =\n        | Backlog\n        | Active\n        | Blocked\n        | InReview\n        | Done\n        | Canceled\n\n        static member GetKnownTypes() = GetKnownTypes<WorkItemStatus>()\n\n    /// Defines the commands for the WorkItem actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type WorkItemCommand =\n        | Create of\n            workItemId: WorkItemId *\n            workItemNumber: WorkItemNumber *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            repositoryId: RepositoryId *\n            title: string *\n            description: string\n        | SetTitle of title: string\n        | SetDescription of description: string\n        | SetStatus of status: WorkItemStatus\n        | AddParticipant of userId: UserId\n        | RemoveParticipant of userId: UserId\n        | AddTag of tag: string\n        | RemoveTag of tag: string\n        | SetConstraints of constraints: string\n        | SetNotes of notes: string\n        | SetArchitecturalNotes of notes: string\n        | SetMigrationNotes of notes: string\n        | AddExternalRef of reference: string\n        | RemoveExternalRef of reference: string\n        | LinkBranch of branchId: BranchId\n        | UnlinkBranch of branchId: BranchId\n        | LinkReference of referenceId: ReferenceId\n        | UnlinkReference of referenceId: ReferenceId\n        | LinkArtifact of artifactId: ArtifactId\n        | UnlinkArtifact of artifactId: ArtifactId\n        | LinkPromotionSet of promotionSetId: PromotionSetId\n        | UnlinkPromotionSet of promotionSetId: PromotionSetId\n        | LinkReviewNotes of reviewNotesId: ReviewNotesId\n        | UnlinkReviewNotes of reviewNotesId: ReviewNotesId\n        | LinkReviewCheckpoint of reviewCheckpointId: ReviewCheckpointId\n        | UnlinkReviewCheckpoint of reviewCheckpointId: ReviewCheckpointId\n        | LinkValidationResult of validationResultId: ValidationResultId\n        | UnlinkValidationResult of validationResultId: ValidationResultId\n\n        static member GetKnownTypes() = GetKnownTypes<WorkItemCommand>()\n\n    /// Defines the events for the WorkItem actor.\n    [<KnownType(\"GetKnownTypes\")>]\n    type WorkItemEventType =\n        | Created of\n            workItemId: WorkItemId *\n            workItemNumber: WorkItemNumber *\n            ownerId: OwnerId *\n            organizationId: OrganizationId *\n            repositoryId: RepositoryId *\n            title: string *\n            description: string\n        | TitleSet of title: string\n        | DescriptionSet of description: string\n        | StatusSet of status: WorkItemStatus\n        | ParticipantAdded of userId: UserId\n        | ParticipantRemoved of userId: UserId\n        | TagAdded of tag: string\n        | TagRemoved of tag: string\n        | ConstraintsSet of constraints: string\n        | NotesSet of notes: string\n        | ArchitecturalNotesSet of notes: string\n        | MigrationNotesSet of notes: string\n        | ExternalRefAdded of reference: string\n        | ExternalRefRemoved of reference: string\n        | BranchLinked of branchId: BranchId\n        | BranchUnlinked of branchId: BranchId\n        | ReferenceLinked of referenceId: ReferenceId\n        | ReferenceUnlinked of referenceId: ReferenceId\n        | ArtifactLinked of artifactId: ArtifactId\n        | ArtifactUnlinked of artifactId: ArtifactId\n        | PromotionSetLinked of promotionSetId: PromotionSetId\n        | PromotionSetUnlinked of promotionSetId: PromotionSetId\n        | ReviewNotesLinked of reviewNotesId: ReviewNotesId\n        | ReviewNotesUnlinked of reviewNotesId: ReviewNotesId\n        | ReviewCheckpointLinked of reviewCheckpointId: ReviewCheckpointId\n        | ReviewCheckpointUnlinked of reviewCheckpointId: ReviewCheckpointId\n        | ValidationResultLinked of validationResultId: ValidationResultId\n        | ValidationResultUnlinked of validationResultId: ValidationResultId\n\n        static member GetKnownTypes() = GetKnownTypes<WorkItemEventType>()\n\n    /// Record that holds the event type and metadata for a WorkItem event.\n    type WorkItemEvent =\n        {\n            /// The WorkItemEventType case that describes the event.\n            Event: WorkItemEventType\n            /// The EventMetadata for the event. EventMetadata includes the Timestamp, CorrelationId, Principal, and a Properties dictionary.\n            Metadata: EventMetadata\n        }\n\n    /// The WorkItemDto is a data transfer object that represents a work item in the system.\n    type WorkItemDto =\n        {\n            Class: string\n            WorkItemId: WorkItemId\n            WorkItemNumber: WorkItemNumber\n            OwnerId: OwnerId\n            OrganizationId: OrganizationId\n            RepositoryId: RepositoryId\n            Title: string\n            Description: string\n            Status: WorkItemStatus\n            Participants: UserId list\n            Tags: string list\n            Constraints: string\n            Notes: string\n            ArchitecturalNotes: string\n            MigrationNotes: string\n            ExternalRefs: string list\n            BranchIds: BranchId list\n            ReferenceIds: ReferenceId list\n            ArtifactIds: ArtifactId list\n            PromotionSetIds: PromotionSetId list\n            ReviewNotesIds: ReviewNotesId list\n            ReviewCheckpointIds: ReviewCheckpointId list\n            ValidationResultIds: ValidationResultId list\n            OnBehalfOf: UserId list\n            CreatedBy: UserId\n            CreatedAt: Instant\n            UpdatedAt: Instant option\n        }\n\n        static member Default =\n            {\n                Class = nameof WorkItemDto\n                WorkItemId = WorkItemId.Empty\n                WorkItemNumber = 0L\n                OwnerId = OwnerId.Empty\n                OrganizationId = OrganizationId.Empty\n                RepositoryId = RepositoryId.Empty\n                Title = String.Empty\n                Description = String.Empty\n                Status = WorkItemStatus.Backlog\n                Participants = []\n                Tags = []\n                Constraints = String.Empty\n                Notes = String.Empty\n                ArchitecturalNotes = String.Empty\n                MigrationNotes = String.Empty\n                ExternalRefs = []\n                BranchIds = []\n                ReferenceIds = []\n                ArtifactIds = []\n                PromotionSetIds = []\n                ReviewNotesIds = []\n                ReviewCheckpointIds = []\n                ValidationResultIds = []\n                OnBehalfOf = []\n                CreatedBy = UserId String.Empty\n                CreatedAt = Constants.DefaultTimestamp\n                UpdatedAt = None\n            }\n\n        /// Updates the WorkItemDto based on the WorkItemEvent.\n        static member UpdateDto workItemEvent currentWorkItemDto =\n            let newWorkItemDto =\n                match workItemEvent.Event with\n                | Created (workItemId, workItemNumber, ownerId, organizationId, repositoryId, title, description) ->\n                    { WorkItemDto.Default with\n                        WorkItemId = workItemId\n                        WorkItemNumber = workItemNumber\n                        OwnerId = ownerId\n                        OrganizationId = organizationId\n                        RepositoryId = repositoryId\n                        Title = title\n                        Description = description\n                        Status = WorkItemStatus.Backlog\n                        CreatedBy = UserId workItemEvent.Metadata.Principal\n                        CreatedAt = workItemEvent.Metadata.Timestamp\n                    }\n                | TitleSet title -> { currentWorkItemDto with Title = title }\n                | DescriptionSet description -> { currentWorkItemDto with Description = description }\n                | StatusSet status -> { currentWorkItemDto with Status = status }\n                | ParticipantAdded userId ->\n                    { currentWorkItemDto with\n                        Participants =\n                            currentWorkItemDto.Participants\n                            |> List.append [ userId ]\n                            |> List.distinct\n                    }\n                | ParticipantRemoved userId ->\n                    { currentWorkItemDto with\n                        Participants =\n                            currentWorkItemDto.Participants\n                            |> List.filter (fun existing -> existing <> userId)\n                    }\n                | TagAdded tag ->\n                    { currentWorkItemDto with\n                        Tags =\n                            currentWorkItemDto.Tags\n                            |> List.append [ tag ]\n                            |> List.distinct\n                    }\n                | TagRemoved tag ->\n                    { currentWorkItemDto with\n                        Tags =\n                            currentWorkItemDto.Tags\n                            |> List.filter (fun existing -> existing <> tag)\n                    }\n                | ConstraintsSet constraints -> { currentWorkItemDto with Constraints = constraints }\n                | NotesSet notes -> { currentWorkItemDto with Notes = notes }\n                | ArchitecturalNotesSet notes -> { currentWorkItemDto with ArchitecturalNotes = notes }\n                | MigrationNotesSet notes -> { currentWorkItemDto with MigrationNotes = notes }\n                | ExternalRefAdded reference ->\n                    { currentWorkItemDto with\n                        ExternalRefs =\n                            currentWorkItemDto.ExternalRefs\n                            |> List.append [ reference ]\n                            |> List.distinct\n                    }\n                | ExternalRefRemoved reference ->\n                    { currentWorkItemDto with\n                        ExternalRefs =\n                            currentWorkItemDto.ExternalRefs\n                            |> List.filter (fun existing -> existing <> reference)\n                    }\n                | BranchLinked branchId ->\n                    { currentWorkItemDto with\n                        BranchIds =\n                            currentWorkItemDto.BranchIds\n                            |> List.append [ branchId ]\n                            |> List.distinct\n                    }\n                | BranchUnlinked branchId ->\n                    { currentWorkItemDto with\n                        BranchIds =\n                            currentWorkItemDto.BranchIds\n                            |> List.filter (fun existing -> existing <> branchId)\n                    }\n                | ReferenceLinked referenceId ->\n                    { currentWorkItemDto with\n                        ReferenceIds =\n                            currentWorkItemDto.ReferenceIds\n                            |> List.append [ referenceId ]\n                            |> List.distinct\n                    }\n                | ReferenceUnlinked referenceId ->\n                    { currentWorkItemDto with\n                        ReferenceIds =\n                            currentWorkItemDto.ReferenceIds\n                            |> List.filter (fun existing -> existing <> referenceId)\n                    }\n                | ArtifactLinked artifactId ->\n                    { currentWorkItemDto with\n                        ArtifactIds =\n                            currentWorkItemDto.ArtifactIds\n                            |> List.append [ artifactId ]\n                            |> List.distinct\n                    }\n                | ArtifactUnlinked artifactId ->\n                    { currentWorkItemDto with\n                        ArtifactIds =\n                            currentWorkItemDto.ArtifactIds\n                            |> List.filter (fun existing -> existing <> artifactId)\n                    }\n                | PromotionSetLinked promotionSetId ->\n                    { currentWorkItemDto with\n                        PromotionSetIds =\n                            currentWorkItemDto.PromotionSetIds\n                            |> List.append [ promotionSetId ]\n                            |> List.distinct\n                    }\n                | PromotionSetUnlinked promotionSetId ->\n                    { currentWorkItemDto with\n                        PromotionSetIds =\n                            currentWorkItemDto.PromotionSetIds\n                            |> List.filter (fun existing -> existing <> promotionSetId)\n                    }\n                | ReviewNotesLinked reviewNotesId ->\n                    { currentWorkItemDto with\n                        ReviewNotesIds =\n                            currentWorkItemDto.ReviewNotesIds\n                            |> List.append [ reviewNotesId ]\n                            |> List.distinct\n                    }\n                | ReviewNotesUnlinked reviewNotesId ->\n                    { currentWorkItemDto with\n                        ReviewNotesIds =\n                            currentWorkItemDto.ReviewNotesIds\n                            |> List.filter (fun existing -> existing <> reviewNotesId)\n                    }\n                | ReviewCheckpointLinked reviewCheckpointId ->\n                    { currentWorkItemDto with\n                        ReviewCheckpointIds =\n                            currentWorkItemDto.ReviewCheckpointIds\n                            |> List.append [ reviewCheckpointId ]\n                            |> List.distinct\n                    }\n                | ReviewCheckpointUnlinked reviewCheckpointId ->\n                    { currentWorkItemDto with\n                        ReviewCheckpointIds =\n                            currentWorkItemDto.ReviewCheckpointIds\n                            |> List.filter (fun existing -> existing <> reviewCheckpointId)\n                    }\n                | ValidationResultLinked validationResultId ->\n                    { currentWorkItemDto with\n                        ValidationResultIds =\n                            currentWorkItemDto.ValidationResultIds\n                            |> List.append [ validationResultId ]\n                            |> List.distinct\n                    }\n                | ValidationResultUnlinked validationResultId ->\n                    { currentWorkItemDto with\n                        ValidationResultIds =\n                            currentWorkItemDto.ValidationResultIds\n                            |> List.filter (fun existing -> existing <> validationResultId)\n                    }\n\n            let onBehalfOf =\n                newWorkItemDto.OnBehalfOf\n                |> List.append [ UserId workItemEvent.Metadata.Principal ]\n                |> List.distinct\n\n            { newWorkItemDto with OnBehalfOf = onBehalfOf; UpdatedAt = Some workItemEvent.Metadata.Timestamp }\n\n    type WorkItemLinksDto =\n        {\n            WorkItemId: WorkItemId\n            WorkItemNumber: WorkItemNumber\n            ReferenceIds: ReferenceId list\n            PromotionSetIds: PromotionSetId list\n            ArtifactIds: ArtifactId list\n            AgentSummaryArtifactIds: ArtifactId list\n            PromptArtifactIds: ArtifactId list\n            ReviewNotesArtifactIds: ArtifactId list\n            OtherArtifactIds: ArtifactId list\n        }\n\n        static member Default =\n            {\n                WorkItemId = WorkItemId.Empty\n                WorkItemNumber = 0L\n                ReferenceIds = []\n                PromotionSetIds = []\n                ArtifactIds = []\n                AgentSummaryArtifactIds = []\n                PromptArtifactIds = []\n                ReviewNotesArtifactIds = []\n                OtherArtifactIds = []\n            }\n"
  },
  {
    "path": "src/Grace.Types.Tests/AGENTS.md",
    "content": "# Grace.Types.Tests Agents Guide\n\nRead `../AGENTS.md` for repo-wide expectations before editing tests here.\n\n## Purpose\n\n- Unit tests for domain contracts in `Grace.Types`.\n- Validate deterministic DTO/event behavior, parser contracts, and pure type-level invariants.\n\n## Test File Organization\n\n- Match tests to type source files when practical:\n  - `Grace.Types/PromotionSet.Types.fs` -> `Grace.Types.Tests/PromotionSet.Types.Tests.fs` and `Grace.Types.Tests/PromotionSet.ConflictModel.Types.Tests.fs`\n  - `Grace.Types/Validation.Types.fs` -> `Grace.Types.Tests/Validation.Types.Tests.fs`\n  - `Grace.Types/Queue.Types.fs` -> `Grace.Types.Tests/Queue.Types.Tests.fs`\n  - `Grace.Types/WorkItem.Types.fs` -> `Grace.Types.Tests/WorkItem.Types.Tests.fs`\n\n## Key Patterns\n\n1. Keep tests deterministic; use fixed timestamps and explicit GUIDs when assertions depend on exact values.\n2. Test public contract semantics rather than implementation internals.\n3. Avoid server or Aspire dependencies in this project.\n\n## Validation\n\n- Run `dotnet build --configuration Release`.\n- Run `dotnet test --no-build --configuration Release src/Grace.Types.Tests/Grace.Types.Tests.fsproj`.\n- Run `dotnet tool run fantomas --recurse .` from `./src` after F# changes.\n"
  },
  {
    "path": "src/Grace.Types.Tests/Automation.Types.Tests.fs",
    "content": "namespace Grace.Types.Tests\n\nopen Grace.Shared\nopen Grace.Shared.Parameters.Common\nopen Grace.Types.Automation\nopen NUnit.Framework\nopen System\n\n[<Parallelizable(ParallelScope.All)>]\ntype AutomationTypesTests() =\n    [<Test>]\n    member _.AutomationEventTypeIncludesAgentLifecycleCases() =\n        let eventTypes = Utilities.listCases<AutomationEventType>() |> Set.ofArray\n\n        Assert.Multiple(fun () ->\n            Assert.That(eventTypes.Contains(\"AgentWorkStarted\"), Is.True)\n            Assert.That(eventTypes.Contains(\"AgentWorkStopped\"), Is.True)\n            Assert.That(eventTypes.Contains(\"AgentSummaryAdded\"), Is.True)\n            Assert.That(eventTypes.Contains(\"AgentBootstrapped\"), Is.True)\n        )\n\n    [<Test>]\n    member _.StartAgentSessionParametersDefaultRequiredFieldsAreEmpty() =\n        let parameters = StartAgentSessionParameters()\n\n        Assert.Multiple(fun () ->\n            Assert.That(parameters.CorrelationId, Is.EqualTo(String.Empty))\n            Assert.That(parameters.WorkItemIdOrNumber, Is.EqualTo(String.Empty))\n            Assert.That(parameters.OperationId, Is.EqualTo(String.Empty))\n            Assert.That(parameters.AgentId, Is.EqualTo(String.Empty))\n        )\n\n    [<Test>]\n    member _.StartAgentSessionParametersRoundTripsSerialization() =\n        let parameters = StartAgentSessionParameters()\n        parameters.CorrelationId <- \"corr-session-start\"\n        parameters.OwnerId <- \"owner-1\"\n        parameters.OrganizationId <- \"org-1\"\n        parameters.RepositoryId <- \"repo-1\"\n        parameters.AgentId <- \"agent-1\"\n        parameters.AgentDisplayName <- \"Codex\"\n        parameters.WorkItemIdOrNumber <- \"42\"\n        parameters.PromotionSetId <- \"promotion-set-9\"\n        parameters.Source <- \"codex\"\n        parameters.OperationId <- \"operation-123\"\n\n        let json = Utilities.serialize parameters\n        let deserialized = Utilities.deserialize<StartAgentSessionParameters> json\n\n        Assert.Multiple(fun () ->\n            Assert.That(deserialized.CorrelationId, Is.EqualTo(\"corr-session-start\"))\n            Assert.That(deserialized.WorkItemIdOrNumber, Is.EqualTo(\"42\"))\n            Assert.That(deserialized.PromotionSetId, Is.EqualTo(\"promotion-set-9\"))\n            Assert.That(deserialized.OperationId, Is.EqualTo(\"operation-123\"))\n        )\n"
  },
  {
    "path": "src/Grace.Types.Tests/Grace.Types.Tests.fsproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <LangVersion>preview</LangVersion>\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n    <GenerateProgramFile>false</GenerateProgramFile>\n    <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>\n    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    <OtherFlags>--test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen</OtherFlags>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"Automation.Types.Tests.fs\" />\n    <Compile Include=\"PromotionSet.ConflictModel.Types.Tests.fs\" />\n    <Compile Include=\"PromotionSet.Types.Tests.fs\" />\n    <Compile Include=\"Validation.Types.Tests.fs\" />\n    <Compile Include=\"Queue.Types.Tests.fs\" />\n    <Compile Include=\"WorkItem.Types.Tests.fs\" />\n    <Compile Include=\"Program.fs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"FsUnit\" Version=\"7.1.1\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration\" Version=\"10.0.0\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\n    <PackageReference Include=\"NUnit\" Version=\"4.4.0\" />\n    <PackageReference Include=\"NUnit3TestAdapter\" Version=\"5.2.0\" />\n    <PackageReference Include=\"NUnit.Analyzers\" Version=\"4.11.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"6.0.4\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Grace.Shared\\Grace.Shared.fsproj\" />\n    <ProjectReference Include=\"..\\Grace.Types\\Grace.Types.fsproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Update=\"FSharp.Core\" Version=\"10.0.100\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Grace.Types.Tests/Program.fs",
    "content": "namespace Grace.Types.Tests\n\nopen System\n\nmodule Program =\n    [<EntryPoint>]\n    let main _ =\n        Console.WriteLine(\"Grace.Types.Tests\")\n        0\n"
  },
  {
    "path": "src/Grace.Types.Tests/PromotionSet.ConflictModel.Types.Tests.fs",
    "content": "namespace Grace.Types.Tests\n\nopen Grace.Types.PromotionSetConflictModel\nopen Microsoft.Extensions.Configuration\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\nopen System.Threading.Tasks\n\n[<TestFixture>]\ntype PromotionSetConflictModelTypesTests() =\n\n    let sampleRequest: ConflictResolutionModelRequest =\n        { FilePath = \"src/app.fs\"; BaseContent = Some \"let value = 1\"; OursContent = Some \"let value = 2\"; TheirsContent = Some \"let value = 3\" }\n\n    [<Test>]\n    member _.CreateProviderDefaultsToNullProviderWhenNotConfigured() : Task =\n        task {\n            let configuration = ConfigurationBuilder().Build()\n            let provider = createProvider configuration\n\n            Assert.That(provider.ProviderName, Is.EqualTo(\"none\"))\n\n            let! result = provider.SuggestResolution sampleRequest\n\n            match result with\n            | Ok _ -> Assert.Fail(\"Expected null provider to return error.\")\n            | Error errorText -> Assert.That(errorText, Does.Contain(\"not configured\"))\n        }\n\n    [<Test>]\n    member _.OpenRouterProviderReturnsDeterministicErrorWhenApiKeyMissing() : Task =\n        task {\n            let apiKeyEnvVarName = \"GRACE_TEST_PROMOTIONSET_OPENROUTER_KEY_MISSING\"\n            let previousApiKeyValue = Environment.GetEnvironmentVariable(apiKeyEnvVarName)\n\n            try\n                Environment.SetEnvironmentVariable(apiKeyEnvVarName, null)\n\n                let settings =\n                    Dictionary<string, string>(\n                        [\n                            KeyValuePair(\"Grace:PromotionSetModels:Provider\", \"OpenRouter\")\n                            KeyValuePair(\"Grace:PromotionSetModels:OpenRouter:ApiBase\", \"https://openrouter.ai/api/v1\")\n                            KeyValuePair(\"Grace:PromotionSetModels:OpenRouter:ApiKeyEnvVar\", apiKeyEnvVarName)\n                            KeyValuePair(\"Grace:PromotionSetModels:OpenRouter:Model\", \"openrouter/auto\")\n                        ]\n                    )\n\n                let configuration =\n                    ConfigurationBuilder()\n                        .AddInMemoryCollection(settings)\n                        .Build()\n\n                let provider = createProvider configuration\n\n                Assert.That(provider.ProviderName, Is.EqualTo(\"OpenRouter\"))\n\n                let! result = provider.SuggestResolution sampleRequest\n\n                match result with\n                | Ok _ -> Assert.Fail(\"Expected missing API key to return an error.\")\n                | Error errorText ->\n                    Assert.That(errorText, Does.Contain(apiKeyEnvVarName))\n                    Assert.That(errorText, Does.Contain(\"not configured\"))\n            finally\n                Environment.SetEnvironmentVariable(apiKeyEnvVarName, previousApiKeyValue)\n        }\n\n    [<Test>]\n    member _.ParseModelResponseRejectsMissingConfidence() =\n        let responseJson = \"\"\"{\"proposedContent\":\"let value = 2\",\"shouldDelete\":false,\"explanation\":\"merge\"}\"\"\"\n\n        match tryParseModelResponse responseJson with\n        | Ok _ -> Assert.Fail(\"Expected parser to reject missing confidence.\")\n        | Error errorText -> Assert.That(errorText, Does.Contain(\"confidence\"))\n\n    [<Test>]\n    member _.ParseModelResponseRejectsOutOfRangeConfidence() =\n        let responseJson = \"\"\"{\"proposedContent\":\"let value = 2\",\"shouldDelete\":false,\"confidence\":1.5,\"explanation\":\"merge\"}\"\"\"\n\n        match tryParseModelResponse responseJson with\n        | Ok _ -> Assert.Fail(\"Expected parser to reject out-of-range confidence.\")\n        | Error errorText -> Assert.That(errorText, Does.Contain(\"[0.0, 1.0]\"))\n\n    [<Test>]\n    member _.ParseModelResponseAcceptsDeleteWithoutProposedContent() =\n        let responseJson = \"\"\"{\"proposedContent\":null,\"shouldDelete\":true,\"confidence\":0.91,\"explanation\":\"delete\"}\"\"\"\n\n        match tryParseModelResponse responseJson with\n        | Error errorText -> Assert.Fail($\"Expected delete response to parse successfully, but got error: {errorText}\")\n        | Ok parsed ->\n            Assert.That(parsed.ShouldDelete, Is.True)\n            Assert.That(parsed.ProposedContent, Is.EqualTo(None))\n            Assert.That(parsed.Confidence, Is.EqualTo(0.91).Within(0.00001))\n"
  },
  {
    "path": "src/Grace.Types.Tests/PromotionSet.Types.Tests.fs",
    "content": "namespace Grace.Types.Tests\n\nopen Grace.Types.PromotionSet\nopen Grace.Types.Types\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype PromotionSetDeterminismTests() =\n\n    let createMetadata correlationId principal timestamp =\n        { Timestamp = timestamp; CorrelationId = correlationId; Principal = principal; Properties = Dictionary<string, string>() }\n\n    let createPromotionSetDto timestamp =\n        let createdEvent: PromotionSetEvent =\n            {\n                Event = PromotionSetEventType.Created(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid())\n                Metadata = createMetadata \"corr-created\" \"tester\" timestamp\n            }\n\n        PromotionSetDto.UpdateDto createdEvent PromotionSetDto.Default\n\n    [<Test>]\n    member _.InputPromotionsUpdatedResetsComputedState() =\n        let timestamp = Instant.FromUtc(2026, 2, 18, 8, 0)\n        let seedDto = createPromotionSetDto timestamp\n\n        let previouslyComputed =\n            { seedDto with\n                Status = PromotionSetStatus.Blocked\n                StepsComputationStatus = StepsComputationStatus.Computed\n                StepsComputationAttempt = 3\n                StepsComputationError = Option.Some \"stale\"\n                ComputedAgainstParentTerminalPromotionReferenceId = Option.Some(Guid.NewGuid())\n            }\n\n        let pointers =\n            [\n                { BranchId = Guid.NewGuid(); ReferenceId = Guid.NewGuid(); DirectoryVersionId = Guid.NewGuid() }\n            ]\n\n        let inputUpdatedEvent: PromotionSetEvent =\n            {\n                Event = PromotionSetEventType.InputPromotionsUpdated pointers\n                Metadata = createMetadata \"corr-input-updated\" \"tester\" (timestamp + Duration.FromMinutes(1.0))\n            }\n\n        let updated = PromotionSetDto.UpdateDto inputUpdatedEvent previouslyComputed\n\n        Assert.That(updated.Steps.Length, Is.EqualTo(1))\n        Assert.That(updated.Status, Is.EqualTo(PromotionSetStatus.Ready))\n        Assert.That(updated.StepsComputationStatus, Is.EqualTo(StepsComputationStatus.NotComputed))\n        Assert.That(updated.ComputedAgainstParentTerminalPromotionReferenceId.IsNone, Is.True)\n        Assert.That(updated.StepsComputationError.IsNone, Is.True)\n        Assert.That(updated.StepsComputationAttempt, Is.EqualTo(3))\n\n    [<Test>]\n    member _.StepsUpdatedIncrementsAttemptAndClearsBlockedStatus() =\n        let timestamp = Instant.FromUtc(2026, 2, 18, 9, 0)\n        let seedDto = createPromotionSetDto timestamp\n\n        let blocked =\n            { seedDto with\n                Status = PromotionSetStatus.Blocked\n                StepsComputationStatus = StepsComputationStatus.ComputeFailed\n                StepsComputationAttempt = 1\n                StepsComputationError = Option.Some \"manual review required\"\n            }\n\n        let promotionSetStep: PromotionSetStep =\n            {\n                StepId = Guid.NewGuid()\n                Order = 0\n                OriginalPromotion = { BranchId = Guid.NewGuid(); ReferenceId = Guid.NewGuid(); DirectoryVersionId = Guid.NewGuid() }\n                OriginalBasePromotionReferenceId = Guid.NewGuid()\n                OriginalBaseDirectoryVersionId = Guid.NewGuid()\n                ComputedAgainstBaseDirectoryVersionId = Guid.NewGuid()\n                AppliedDirectoryVersionId = Guid.NewGuid()\n                ConflictSummaryArtifactId = Option.None\n                ConflictStatus = StepConflictStatus.AutoResolved\n            }\n\n        let computedAgainstTerminalReferenceId = Guid.NewGuid()\n\n        let stepsUpdatedEvent: PromotionSetEvent =\n            {\n                Event = PromotionSetEventType.StepsUpdated([ promotionSetStep ], computedAgainstTerminalReferenceId)\n                Metadata = createMetadata \"corr-steps-updated\" \"tester\" (timestamp + Duration.FromMinutes(2.0))\n            }\n\n        let updated = PromotionSetDto.UpdateDto stepsUpdatedEvent blocked\n\n        Assert.That(updated.Status, Is.EqualTo(PromotionSetStatus.Ready))\n        Assert.That(updated.StepsComputationStatus, Is.EqualTo(StepsComputationStatus.Computed))\n        Assert.That(updated.StepsComputationAttempt, Is.EqualTo(2))\n        Assert.That(updated.StepsComputationError.IsNone, Is.True)\n        Assert.That(updated.ComputedAgainstParentTerminalPromotionReferenceId, Is.EqualTo(Option.Some computedAgainstTerminalReferenceId))\n"
  },
  {
    "path": "src/Grace.Types.Tests/Queue.Types.Tests.fs",
    "content": "namespace Grace.Types.Tests\n\nopen Grace.Types.Policy\nopen Grace.Types.Queue\nopen Grace.Types.Types\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype QueueTypesTests() =\n    let metadata (timestamp: Instant) : EventMetadata =\n        { Timestamp = timestamp; CorrelationId = \"corr-queue\"; Principal = \"tester\"; Properties = Dictionary<string, string>() }\n\n    [<Test>]\n    member _.PromotionQueueDtoUpdatesDeterministically() =\n        let branchId = Guid.NewGuid()\n        let promotionSetId = Guid.NewGuid()\n        let snapshotId = PolicySnapshotId \"policy\"\n\n        let events: PromotionQueueEvent list =\n            [\n                { Event = PromotionQueueEventType.Initialized(branchId, snapshotId); Metadata = metadata (Instant.FromUtc(2025, 1, 1, 0, 0)) }\n                { Event = PromotionQueueEventType.PromotionSetEnqueued promotionSetId; Metadata = metadata (Instant.FromUtc(2025, 1, 1, 0, 1)) }\n                { Event = PromotionQueueEventType.RunningPromotionSetSet(Some promotionSetId); Metadata = metadata (Instant.FromUtc(2025, 1, 1, 0, 2)) }\n                { Event = PromotionQueueEventType.Paused; Metadata = metadata (Instant.FromUtc(2025, 1, 1, 0, 3)) }\n                { Event = PromotionQueueEventType.Resumed; Metadata = metadata (Instant.FromUtc(2025, 1, 1, 0, 4)) }\n            ]\n\n        let finalState =\n            events\n            |> List.fold (fun state ev -> PromotionQueueDto.UpdateDto ev state) PromotionQueue.Default\n\n        let secondPass =\n            events\n            |> List.fold (fun state ev -> PromotionQueueDto.UpdateDto ev state) PromotionQueue.Default\n\n        Assert.That(finalState, Is.EqualTo(secondPass))\n        Assert.That(finalState.TargetBranchId, Is.EqualTo(branchId))\n        Assert.That(finalState.PromotionSetIds.Length, Is.EqualTo(1))\n        Assert.That(finalState.PromotionSetIds[0], Is.EqualTo(promotionSetId))\n        Assert.That(finalState.State, Is.EqualTo(QueueState.Running))\n\n    [<Test>]\n    member _.PromotionSetDequeuedRemovesFromQueue() =\n        let promotionSetId = Guid.NewGuid()\n\n        let queue =\n            { PromotionQueue.Default with\n                TargetBranchId = Guid.NewGuid()\n                PromotionSetIds = [ promotionSetId ]\n                RunningPromotionSetId = Some promotionSetId\n                State = QueueState.Running\n            }\n\n        let event: PromotionQueueEvent =\n            { Event = PromotionQueueEventType.PromotionSetDequeued promotionSetId; Metadata = metadata (Instant.FromUtc(2025, 1, 5, 0, 0)) }\n\n        let updated = PromotionQueueDto.UpdateDto event queue\n\n        Assert.That(updated.PromotionSetIds, Is.Empty)\n"
  },
  {
    "path": "src/Grace.Types.Tests/Validation.Types.Tests.fs",
    "content": "namespace Grace.Types.Tests\n\nopen Grace.Types.Types\nopen Grace.Types.Validation\nopen NUnit.Framework\nopen NodaTime\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype ValidationQuickScanDeterminism() =\n    let metadata timestamp = { Timestamp = timestamp; CorrelationId = \"corr-1\"; Principal = \"tester\"; Properties = Dictionary<string, string>() }\n\n    [<Test>]\n    member _.UpdateUsesEventPayloadAndTimestamp() =\n        let timestamp = Instant.FromUtc(2025, 1, 1, 0, 0)\n        let validationResultId = Guid.NewGuid()\n\n        let validationResult =\n            { ValidationResultDto.Default with\n                ValidationResultId = validationResultId\n                ValidationName = \"quick-scan\"\n                ValidationVersion = \"1.0\"\n                Output = { Status = ValidationStatus.Pass; Summary = \"quick-scan complete\"; ArtifactIds = [] }\n                CreatedAt = timestamp\n            }\n\n        let validationEvent: ValidationResultEvent = { Event = ValidationResultEventType.Recorded validationResult; Metadata = metadata timestamp }\n\n        let updated = ValidationResultDto.UpdateDto validationEvent ValidationResultDto.Default\n\n        Assert.That(updated.ValidationResultId, Is.EqualTo(validationResult.ValidationResultId))\n        Assert.That(updated.ValidationName, Is.EqualTo(validationResult.ValidationName))\n        Assert.That(updated.Output, Is.EqualTo(validationResult.Output))\n        Assert.That(updated.UpdatedAt, Is.EqualTo(Some timestamp))\n        Assert.That(updated.OnBehalfOf, Is.EquivalentTo([ UserId \"tester\" ]))\n\n    [<Test>]\n    member _.UpdateIsDeterministicForSameEvent() =\n        let timestamp = Instant.FromUtc(2025, 2, 1, 0, 0)\n        let validationResultId = Guid.NewGuid()\n\n        let validationResult =\n            { ValidationResultDto.Default with\n                ValidationResultId = validationResultId\n                ValidationName = \"quick-scan\"\n                ValidationVersion = \"1.0\"\n                Output = { Status = ValidationStatus.Pass; Summary = \"quick-scan complete\"; ArtifactIds = [] }\n                CreatedAt = timestamp\n            }\n\n        let validationEvent: ValidationResultEvent = { Event = ValidationResultEventType.Recorded validationResult; Metadata = metadata timestamp }\n\n        let first = ValidationResultDto.UpdateDto validationEvent ValidationResultDto.Default\n        let second = ValidationResultDto.UpdateDto validationEvent ValidationResultDto.Default\n\n        Assert.That(first, Is.EqualTo(second))\n"
  },
  {
    "path": "src/Grace.Types.Tests/WorkItem.Types.Tests.fs",
    "content": "namespace Grace.Types.Tests\n\nopen Grace.Types.Types\nopen Grace.Types.WorkItem\nopen NodaTime\nopen NUnit.Framework\nopen System\nopen System.Collections.Generic\n\n[<Parallelizable(ParallelScope.All)>]\ntype WorkItemTypesTests() =\n    let metadata timestamp = { Timestamp = timestamp; CorrelationId = \"corr-work-item\"; Principal = \"tester\"; Properties = Dictionary<string, string>() }\n\n    [<Test>]\n    member _.UpdateDtoPreservesCreatedFields() =\n        let createdAt = Instant.FromUtc(2025, 1, 1, 0, 0)\n        let updatedAt = Instant.FromUtc(2025, 1, 2, 0, 0)\n        let createdBy = UserId \"creator\"\n\n        let dto = { WorkItemDto.Default with WorkItemId = Guid.NewGuid(); Title = \"Before\"; CreatedAt = createdAt; CreatedBy = createdBy }\n\n        let workItemEvent = { Event = WorkItemEventType.TitleSet \"After\"; Metadata = metadata updatedAt }\n\n        let updated = WorkItemDto.UpdateDto workItemEvent dto\n\n        Assert.That(updated.Title, Is.EqualTo(\"After\"))\n        Assert.That(updated.CreatedAt, Is.EqualTo(createdAt))\n        Assert.That(updated.CreatedBy, Is.EqualTo(createdBy))\n        Assert.That(updated.UpdatedAt, Is.EqualTo(Some updatedAt))\n\n    [<Test>]\n    member _.CreatedEventSetsWorkItemNumber() =\n        let createdAt = Instant.FromUtc(2025, 2, 1, 0, 0)\n        let workItemId = Guid.NewGuid()\n        let workItemNumber = 42L\n\n        let createdEvent =\n            {\n                Event =\n                    WorkItemEventType.Created(\n                        workItemId,\n                        workItemNumber,\n                        Guid.NewGuid(),\n                        Guid.NewGuid(),\n                        Guid.NewGuid(),\n                        \"Title\",\n                        \"Description\"\n                    )\n                Metadata = metadata createdAt\n            }\n\n        let dto = WorkItemDto.UpdateDto createdEvent WorkItemDto.Default\n\n        Assert.That(dto.WorkItemId, Is.EqualTo(workItemId))\n        Assert.That(dto.WorkItemNumber, Is.EqualTo(workItemNumber))\n"
  },
  {
    "path": "src/Grace.slnx",
    "content": "<Solution>\n  <Configurations>\n    <Platform Name=\"Any CPU\" />\n    <Platform Name=\"x64\" />\n    <Platform Name=\"x86\" />\n  </Configurations>\n  <Folder Name=\"/Solution items/\">\n    <File Path=\"agents.md\" />\n    <File Path=\"Directory.Build.props\" />\n  </Folder>\n  <Project Path=\"Grace.Actors/Grace.Actors.fsproj\" />\n  <Project Path=\"Grace.Aspire.AppHost/Grace.Aspire.AppHost.csproj\" />\n  <Project Path=\"Grace.Authorization.Tests/Grace.Authorization.Tests.fsproj\" />\n  <Project Path=\"Grace.CLI.Tests/Grace.CLI.Tests.fsproj\" />\n  <Project Path=\"Grace.CLI/Grace.CLI.fsproj\" />\n  <Project Path=\"Grace.Load/Grace.Load.fsproj\" />\n  <Project Path=\"Grace.Orleans.CodeGen/Grace.Orleans.CodeGen.csproj\" />\n  <Project Path=\"Grace.SDK/Grace.SDK.fsproj\" />\n  <Project Path=\"Grace.Server.Tests/Grace.Server.Tests.fsproj\" />\n  <Project Path=\"Grace.Types.Tests/Grace.Types.Tests.fsproj\" />\n  <Project Path=\"Grace.Server/Grace.Server.fsproj\">\n    <BuildDependency Project=\"Grace.Shared/Grace.Shared.fsproj\" />\n  </Project>\n  <Project Path=\"Grace.Shared/Grace.Shared.fsproj\" />\n  <Project Path=\"Grace.Types/Grace.Types.fsproj\" />\n</Solution>\n"
  },
  {
    "path": "src/OpenAPI/Branch.Components.OpenAPI.yaml",
    "content": "    BranchParameters:\n      description: Parameters for many endpoints in the /branch path.\n      type: object\n      properties:\n        ownerId:\n          type: string\n        ownerName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OwnerName'\n        organizationId:\n          type: string\n        organizationName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationName'\n        repositoryId:\n          type: string\n        repositoryName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryName'\n        branchId:\n          type: string\n        branchName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/BranchName'\n    BranchQueryParameters:\n      description: Base class for parameters for branch queries.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            sha256Hash:\n              $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash'\n            referenceId:\n              type: string\n    CreateBranchParameters:\n      description: Parameters for the /branch/create endpoint.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            parentBranchId:\n              type: string\n            parentBranchName:\n              $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/BranchName'\n    RebaseParameters:\n      description: Parameters for the /branch/rebase endpoint.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            basedOn:\n              $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId'\n    CreateReferenceParameters:\n      description: Parameters for the various /branch/create[reference] endpoints.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            directoryId:\n              $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId'\n            sha256Hash:\n              $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash'\n            message:\n              type: string\n    SetBranchNameParameters:\n      description: Parameters for the /branch/setName endpoint.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            newName:\n              type: string\n    EnableFeatureParameters:\n      description: Parameters for the various /branch/enable[feature] endpoints.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            enabled:\n              type: boolean\n    DeleteBranchParameters:\n      description: Parameters for the /branch/delete endpoint.\n      allOf:\n        - $ref: '#/BranchParameters'\n    GetReferenceParameters:\n      description: Parameters for the /branch/getReference endpoint.\n      allOf:\n        - $ref: '#/BranchQueryParameters'\n    GetReferencesParameters:\n      description: Parameters for the /branch/getReferences and /branch/get[reference] endpoints.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            fullSha:\n              type: boolean\n            maxCount:\n              type: integer\n    GetDiffsForReferenceTypeParameters:\n      description: Parameters for the /branch/getDiffsForReferenceType endpoint.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            referenceType:\n              type: string\n            maxCount:\n              type: integer\n    GetDiffsForReferencesParameters:\n      description: Parameters for the /branch/getDiffsForReferences endpoint.\n      allOf:\n        - $ref: '#/BranchParameters'\n        - type: object\n          properties:\n            references:\n              type: string\n            maxCount:\n              type: integer\n    GetBranchParameters:\n      description: Parameters for the /branch/get endpoint.\n      allOf:\n        - $ref: '#/BranchQueryParameters'\n    SwitchParameters:\n      description: Parameters for the /branch/switch endpoint.\n      allOf:\n        - $ref: '#/BranchQueryParameters'\n    GetBranchVersionParameters:\n      description: Parameters for the /branch/getVersion endpoint.\n      allOf:\n        - $ref: '#/BranchQueryParameters'\n"
  },
  {
    "path": "src/OpenAPI/Branch.Paths.OpenAPI.yaml",
    "content": "  /branch/create:\n    post:\n      summary: Creates a new branch.\n      description: Creates a new branch with the specified name, based on the specified parent branch.\n      operationId: Create\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_13\n                - allOf: &ref_0\n                    - type: object\n                      properties: &ref_10\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    ParentBranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    ParentBranchName:\n                      type: string\n                      example: MyBranch\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/rebase:\n    post:\n      summary: Rebases a branch on its parent branch.\n      description: Rebases a branch on its parent branch.\n      operationId: Rebase\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_14\n                - allOf: *ref_0\n                - type: object\n                  properties:\n                    BasedOn:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/promote:\n    post:\n      summary: Creates a promotion reference in the parent of the specified branch, based on the most-recent commit.\n      description: Creates a promotion reference in the parent of the specified branch, based on the most-recent commit.\n      operationId: Promote\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_1\n                - allOf: *ref_0\n                - type: object\n                  properties:\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    Message:\n                      type: string\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/commit:\n    post:\n      summary: Creates a commit reference pointing to the current root directory version in the branch.\n      description: Creates a commit reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf: *ref_1\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/checkpoint:\n    post:\n      summary: Creates a checkpoint reference pointing to the current root directory version in the branch.\n      description: Creates a checkpoint reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf: *ref_1\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/save:\n    post:\n      summary: Creates a save reference pointing to the current root directory version in the branch.\n      description: Creates a save reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf: *ref_1\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/tag:\n    post:\n      summary: Creates a tag reference pointing to the current root directory version in the branch.\n      description: Creates a tag reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf: *ref_1\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/enablePromotion:\n    post:\n      summary: Enables or disables promotion for the specified branch.\n      description: Enables or disables promotion for the specified branch.\n      operationId: EnablePromotion\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_2\n                - allOf: *ref_0\n                - type: object\n                  properties:\n                    Enabled:\n                      type: boolean\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableCommit:\n    post:\n      summary: Enables or disables commit for the specified branch.\n      description: Enables or disables commit for the specified branch.\n      operationId: EnableCommit\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_2\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableCheckpoint:\n    post:\n      summary: Enables or disables checkpoint for the specified branch.\n      description: Enables or disables checkpoint for the specified branch.\n      operationId: EnableCheckpoint\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_2\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableSave:\n    post:\n      summary: Enables or disables save for the specified branch.\n      description: Enables or disables save for the specified branch.\n      operationId: EnableSave\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_2\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableTag:\n    post:\n      summary: Enables or disables tag for the specified branch.\n      description: Enables or disables tag for the specified branch.\n      operationId: EnableTag\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_2\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/delete:\n    post:\n      summary: Deletes the specified branch.\n      description: Deletes the specified branch.\n      operationId: Delete\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_15\n                - allOf: *ref_0\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/get:\n    post:\n      summary: Gets the specified branch.\n      description: Gets the specified branch.\n      operationId: Get\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_3\n                - allOf: *ref_0\n                - type: object\n                  properties:\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                properties: &ref_4\n                  Class:\n                    type: string\n                    example: BranchDto\n                  BranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  BranchName:\n                    type: string\n                    example: MyBranch\n                  ParentBranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  BasedOn:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  RepositoryId:\n                    type: string\n                    format: uuid\n                    example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                  UserId:\n                    type: string\n                  PromotionEnabled:\n                    type: boolean\n                  CommitEnabled:\n                    type: boolean\n                  CheckpointEnabled:\n                    type: boolean\n                  SaveEnabled:\n                    type: boolean\n                  TagEnabled:\n                    type: boolean\n                  LatestPromotion:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestCommit:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestCheckpoint:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestSave:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  CreatedAt:\n                    type: string\n                    format: date-time\n                  UpdatedAt:\n                    type: string\n                    format: date-time\n                    nullable: true\n                  DeletedAt:\n                    type: string\n                    format: date-time\n                    nullable: true\n                required: &ref_5\n                  - Class\n                  - BranchId\n                  - BranchName\n                  - ParentBranchId\n                  - BasedOn\n                  - RepositoryId\n                  - UserId\n                  - PromotionEnabled\n                  - CommitEnabled\n                  - CheckpointEnabled\n                  - SaveEnabled\n                  - TagEnabled\n                  - LatestPromotion\n                  - LatestCommit\n                  - LatestCheckpoint\n                  - LatestSave\n                  - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getParentBranch:\n    post:\n      summary: Gets the parent branch of the specified branch.\n      description: Gets the parent branch of the specified branch.\n      operationId: GetParentBranch\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_3\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                properties: *ref_4\n                required: *ref_5\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getReference:\n    post:\n      summary: Gets the specified reference.\n      description: Gets the specified reference.\n      operationId: GetReference\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_17\n                - allOf: *ref_3\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                properties: &ref_6\n                  Class:\n                    type: string\n                    example: ReferenceDto\n                  ReferenceId:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  BranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  DirectoryId:\n                    type: string\n                    format: uuid\n                    example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                  Sha256Hash:\n                    type: string\n                    example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                  ReferenceType:\n                    type: string\n                    enum: &ref_9\n                      - Promotion\n                      - Commit\n                      - Checkpoint\n                      - Save\n                      - Tag\n                    example: Commit\n                  ReferenceText:\n                    type: string\n                  CreatedAt:\n                    type: string\n                    format: date-time\n                required: &ref_7\n                  - Class\n                  - ReferenceId\n                  - BranchId\n                  - DirectoryId\n                  - Sha256Hash\n                  - ReferenceType\n                  - ReferenceText\n                  - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getReferences:\n    post:\n      summary: Gets the references for the specified branch.\n      description: Gets the references for the specified branch.\n      operationId: GetReferences\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: &ref_8\n                - allOf: *ref_0\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties: *ref_6\n                  required: *ref_7\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getPromotions:\n    post:\n      summary: Gets the promotions for the specified branch.\n      description: Gets the promotions for the specified branch.\n      operationId: GetPromotions\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_8\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties: *ref_6\n                  required: *ref_7\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getCommits:\n    post:\n      summary: Gets the commits for the specified branch.\n      description: Gets the commits for the specified branch.\n      operationId: GetCommits\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_8\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties: *ref_6\n                  required: *ref_7\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getCheckpoints:\n    post:\n      summary: Gets the checkpoints for the specified branch.\n      description: Gets the checkpoints for the specified branch.\n      operationId: GetCheckpoints\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_8\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties: *ref_6\n                  required: *ref_7\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getSaves:\n    post:\n      summary: Gets the saves for the specified branch.\n      description: Gets the saves for the specified branch.\n      operationId: GetSaves\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_8\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties: *ref_6\n                  required: *ref_7\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getTags:\n    post:\n      summary: Gets the tags for the specified branch.\n      description: Gets the tags for the specified branch.\n      operationId: GetTags\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf: *ref_8\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties: *ref_6\n                  required: *ref_7\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n"
  },
  {
    "path": "src/OpenAPI/Diff.Components.OpenAPI.yaml",
    "content": "    DiffParameters:\n      description: Parameters for diff endpoints.\n      allOf:\n        - $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CommonParameters'\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OwnerName'\n        OrganizationId:\n          type: string\n        OrganizationName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationName'\n        RepositoryId:\n          type: string\n        RepositoryName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryName'\n        DirectoryId1:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId'\n        DirectoryId2:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId'\n    PopulateParameters:\n      description: Parameters for diff population.\n      allOf:\n        - $ref: './Diff.Components.OpenAPI.yaml#/DiffParameters'\n    GetDiffParameters:\n      description: Parameters for retrieving a diff.\n      allOf:\n        - $ref: './Diff.Components.OpenAPI.yaml#/DiffParameters'\n    GetDiffByReferenceTypeParameters:\n      description: Parameters for retrieving a diff by reference type.\n      allOf:\n        - $ref: './Diff.Components.OpenAPI.yaml#/DiffParameters'\n      properties:\n        BranchId:\n          type: string\n        BranchName:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/BranchName'\n    GetDiffBySha256HashParameters:\n      description: Parameters for retrieving a diff by SHA256 hash.\n      allOf:\n        - $ref: './Diff.Components.OpenAPI.yaml#/DiffParameters'\n      properties:\n        Sha256Hash1:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash'\n        Sha256Hash2:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash'\n"
  },
  {
    "path": "src/OpenAPI/Diff.Paths.OpenAPI.yaml",
    "content": "  /diff/populate:\n    post:\n      summary: Populates the diff actor without returning the diff.\n      description: |\n        Populates the diff actor without returning the diff. This endpoint is meant to be used when generating the diff through reacting to an event.\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Diff.Components.OpenAPI.yaml#/PopulateParameters\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /diff/getDiff:\n    post:\n      summary: Retrieves the contents of the diff.\n      description: |\n        Retrieves the contents of the diff between two directories.\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Diff.Components.OpenAPI.yaml#/GetDiffParameters\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /diff/getDiffBySha256Hash:\n    post:\n      summary: Retrieves a diff taken by comparing two DirectoryVersions by Sha256Hash.\n      description: |\n        Retrieves a diff taken by comparing two DirectoryVersions identified by their SHA-256 hash.\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Diff.Components.OpenAPI.yaml#/GetDiffBySha256HashParameters\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n"
  },
  {
    "path": "src/OpenAPI/Directory.Components.OpenAPI.yaml",
    "content": "    DirectoryParameters:\n      description: Parameters for many endpoints in the /directory path.\n      type: object\n      properties:\n        DirectoryId:\n          type: string\n          description: Unique identifier of the directory.\n      allOf:\n        - $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CommonParameters'\n    \n    CreateParameters:\n      description: Parameters for the /directory/create endpoint.\n      type: object\n      properties:\n        DirectoryVersion:\n          $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryVersion'\n      allOf:\n        - $ref: './Directory.Components.OpenAPI.yaml#/DirectoryParameters'\n    \n    GetParameters:\n      description: Parameters for the /directory/get endpoint.\n      type: object\n      properties:\n        RepositoryId:\n          type: string\n          description: Unique identifier of the repository.\n      allOf:\n        - $ref: './Directory.Components.OpenAPI.yaml#/DirectoryParameters'\n    \n    GetByDirectoryIdsParameters:\n      description: Parameters for the /directory/getByDirectoryIds endpoint.\n      type: object\n      properties:\n        RepositoryId:\n          type: string\n          description: Unique identifier of the repository.\n        DirectoryIds:\n          type: array\n          items:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId'\n      allOf:\n        - $ref: './Directory.Components.OpenAPI.yaml#/DirectoryParameters'\n    \n    GetBySha256HashParameters:\n      description: Parameters for the /directory/getBySha256Hash endpoint.\n      type: object\n      properties:\n        RepositoryId:\n          type: string\n          description: Unique identifier of the repository.\n        Sha256Hash:\n          type: string\n          description: The SHA256 hash value.\n      allOf:\n        - $ref: './Directory.Components.OpenAPI.yaml#/DirectoryParameters'\n    \n    SaveDirectoryVersionsParameters:\n      description: Parameters for the /directory/saveDirectoryVersions endpoint.\n      type: object\n      properties:\n        DirectoryVersions:\n          type: array\n          items:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryVersion'\n      allOf:\n        - $ref: './Directory.Components.OpenAPI.yaml#/DirectoryParameters'\n"
  },
  {
    "path": "src/OpenAPI/Directory.Paths.OpenAPI.yaml",
    "content": "  /directory/create:\n    post:\n      summary: Create a new directory version.\n      description: |\n        ### Validation rules\n\n        - `DirectoryVersion.DirectoryId` should be a valid and non-empty GUID.\n        - `DirectoryVersion.RepositoryId` should be a valid and non-empty GUID.\n        - The repository specified by `DirectoryVersion.RepositoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Directory.Components.OpenAPI.yaml#/CreateParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /directory/get:\n    post:\n      summary: Get a directory version.\n      description: |\n        ### Validation rules\n\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - `DirectoryId` should be a valid and non-empty GUID.\n        - The repository specified by `RepositoryId` should exist.\n        - The directory specified by `DirectoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Directory.Components.OpenAPI.yaml#/GetParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryVersion'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /directory/getDirectoryVersionsRecursive:\n    post:\n      summary: Get a directory version and all of its children.\n      description: |\n        ### Validation rules\n\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - `DirectoryId` should be a valid and non-empty GUID.\n        - The repository specified by `RepositoryId` should exist.\n        - The directory specified by `DirectoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Directory.Components.OpenAPI.yaml#/GetParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryVersion'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /directory/getByDirectoryIds:\n    post:\n      summary: Get a list of directory versions by directory IDs.\n      description: |\n        ### Validation rules\n\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - The repository specified by `RepositoryId` should exist.\n        - The directories specified by `DirectoryIds` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Directory.Components.OpenAPI.yaml#/GetByDirectoryIdsParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryVersion'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /directory/getBySha256Hash:\n    post:\n      summary: Get a directory version by its SHA256 hash.\n      description: |\n        ### Validation rules\n\n        - `DirectoryId` should be a valid and non-empty GUID.\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - `Sha256Hash` should not be empty.\n        - The repository specified by `RepositoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Directory.Components.OpenAPI.yaml#/GetBySha256HashParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /directory/saveDirectoryVersions:\n    post:\n      summary: Save a list of directory versions.\n      description: |\n        ### Validation rules\n\n        - Each `DirectoryVersion` in the `DirectoryVersions` list should have valid properties:\n          - `DirectoryVersion.DirectoryId` should be a valid and non-empty GUID.\n          - `DirectoryVersion.RepositoryId` should be a valid and non-empty GUID.\n          - `DirectoryVersion.Sha256Hash` should not be empty.\n          - `DirectoryVersion.RelativePath` must not be empty.\n          - The repository specified by `DirectoryVersion.RepositoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Directory.Components.OpenAPI.yaml#/SaveDirectoryVersionsParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n"
  },
  {
    "path": "src/OpenAPI/Dto.Components.OpenAPI.yaml",
    "content": "    BranchDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        BranchId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/BranchId\n        BranchName:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/BranchName\n        ParentBranchId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/BranchId\n        BasedOn:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId\n        RepositoryId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryId\n        UserId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/UserId\n        PromotionEnabled:\n          type: boolean\n        CommitEnabled:\n          type: boolean\n        CheckpointEnabled:\n          type: boolean\n        SaveEnabled:\n          type: boolean\n        TagEnabled:\n          type: boolean\n        LatestPromotion:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId\n        LatestCommit:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId\n        LatestCheckpoint:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId\n        LatestSave:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId\n        CreatedAt:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        UpdatedAt:\n          type: object\n          properties:\n            value:\n              $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        DeletedAt:\n          type: object\n          properties:\n            value:\n              $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n\n    DiffDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        HasDifferences:\n          type: boolean\n        DirectoryId1:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId\n        Directory1CreatedAt:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        DirectoryId2:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId\n        Directory2CreatedAt:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        Differences:\n          type: array\n          items:\n            $ref: Shared.Components.OpenAPI.yaml#/components/schemas/FileSystemDifference\n        FileDiffs:\n          type: array\n          items:\n            $ref: Shared.Components.OpenAPI.yaml#/components/schemas/FileDiff\n\n    OrganizationDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        OrganizationId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationId\n        OrganizationName:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationName\n        OwnerId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OwnerId\n        OrganizationType:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationType\n        Description:\n          type: string\n        SearchVisibility:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/SearchVisibility\n        Repositories:\n          type: object\n          additionalProperties:\n            $ref: Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryName\n        CreatedAt:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        UpdatedAt:\n          type: object\n          properties:\n            value:\n              $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        DeletedAt:\n          type: object\n          properties:\n            value:\n              $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        DeleteReason:\n          type: string\n\n    OwnerDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        OwnerId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OwnerId\n        OwnerName:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OwnerName\n        OwnerType:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OwnerType\n        Description:\n          type: string\n        SearchVisibility:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/SearchVisibility\n        Organizations:\n          type: object\n          additionalProperties:\n            $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationName\n        CreatedAt:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        UpdatedAt:\n          type: object\n          properties:\n            value:\n              $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        DeletedAt:\n          type: object\n          properties:\n            value:\n              $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n        DeleteReason:\n          type: string\n\n    ReferenceDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        ReferenceId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId\n        BranchId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/BranchId\n        DirectoryId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId\n        Sha256Hash:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash\n        ReferenceType:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceType\n        ReferenceText:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceText\n        CreatedAt:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Instant\n\n    RepositoryDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        RepositoryId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryId\n        OwnerId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OwnerId\n        OrganizationId:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationId\n        RepositoryName:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryName\n        ObjectStorageProvider:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/ObjectStorageProvider\n        StorageAccountName:\n          type: string\n        StorageContainerName:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/StorageContainerName\n        RepositoryVisibility:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryVisibility\n        RepositoryStatus:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryStatus\n        Branches:\n          type: array\n          items:\n            $ref: Shared.Components.OpenAPI.yaml#/components/schemas/BranchName\n        DefaultServerApiVersion:\n          type: string\n        DefaultBranchName:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/BranchName\n        SaveDays:\n          type: number\n          format: double\n        CheckpointDays:\n          type: number\n         "
  },
  {
    "path": "src/OpenAPI/Grace.OpenAPI.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  title: Grace Server API\n  description: |-\n    ## Grace Server API\n    This OpenAPI specification describes the Web API for Grace Server.\n    ---\n    ## Helpful hints\n    * In general, when the parameters ask for a _somethingId_ and a _somethingName_, like, OwnerId and OwnerName, or OrganizationId and OrganizationName, etc. you only need to give one or the other.\n    * If you have the Id's available just as readily as the names, you'll get better performance by using the Id's. It saves a lookup - whether in-memory cache, or database call - at the server.\n    * If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's in Grace are just strings, and feel free to send any other kind of CorrelationId you prefer.\n    * A valid name in Grace has between 2 and 64 characters, has a letter for the first character `^[A-Za-z]`, and letters, numbers, or a dash (-) for the rest `[A-Za-z0-9\\-_]{1,63}`.\n    * The full regex for valid Grace names is: `^[A-Za-z][A-Za-z0-9\\-]{1,63}$`. The ChatGPT explanation of it can be found [here](https://chat.openai.com/share/1d18c634-45ed-4ef5-bd8e-93391f74b637).\n    * If you notice a difference between the OpenAPI spec and the actual API, please file an issue so we can fix it. It's absolutely our intention to keep the spec and the API in sync.\n    ---\n    Please let us know if you have any questions or comments @ [Discussions](https://github.com/scottarbeit/grace/discussions) in the [Grace repository](https://github.com/scottarbeit/grace).\n  contact:\n    name: Scott Arbeit\n    url: https://twitter.com/scottarbeit\n    email: scott.arbeit@outlook.com\n  license:\n    name: MIT\n    url: https://opensource.org/licenses/MIT\n  version: '0.1'\nservers:\n  - url: http://localhost:5000\n    description: Local development server\nsecurity:\n  - bearerAuth: []\npaths:\n  /openApi:\n    get:\n      summary: Get the OpenAPI specification for the Grace Server API\n      description: This endpoint returns the OpenAPI specification for the Grace Server API. The specification is generated from the OpenAPI specification files in the src/OpenAPI folder.\n      operationId: GetOpenApi\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n  /branch/create:\n    post:\n      summary: Creates a new branch.\n      description: Creates a new branch with the specified name, based on the specified parent branch.\n      operationId: Create\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    ParentBranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    ParentBranchName:\n                      type: string\n                      example: MyBranch\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/rebase:\n    post:\n      summary: Rebases a branch on its parent branch.\n      description: Rebases a branch on its parent branch.\n      operationId: Rebase\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    BasedOn:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/promote:\n    post:\n      summary: Creates a promotion reference in the parent of the specified branch, based on the most-recent commit.\n      description: Creates a promotion reference in the parent of the specified branch, based on the most-recent commit.\n      operationId: Promote\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    Message:\n                      type: string\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/commit:\n    post:\n      summary: Creates a commit reference pointing to the current root directory version in the branch.\n      description: Creates a commit reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    Message:\n                      type: string\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/checkpoint:\n    post:\n      summary: Creates a checkpoint reference pointing to the current root directory version in the branch.\n      description: Creates a checkpoint reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    Message:\n                      type: string\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/save:\n    post:\n      summary: Creates a save reference pointing to the current root directory version in the branch.\n      description: Creates a save reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    Message:\n                      type: string\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/tag:\n    post:\n      summary: Creates a tag reference pointing to the current root directory version in the branch.\n      description: Creates a tag reference pointing to the current root directory version in the branch.\n      requestBody:\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    Message:\n                      type: string\n      responses:\n        '200':\n          description: Success\n        '400':\n          description: Bad Request\n        '500':\n          description: Server Error\n  /branch/enablePromotion:\n    post:\n      summary: Enables or disables promotion for the specified branch.\n      description: Enables or disables promotion for the specified branch.\n      operationId: EnablePromotion\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Enabled:\n                      type: boolean\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableCommit:\n    post:\n      summary: Enables or disables commit for the specified branch.\n      description: Enables or disables commit for the specified branch.\n      operationId: EnableCommit\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Enabled:\n                      type: boolean\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableCheckpoint:\n    post:\n      summary: Enables or disables checkpoint for the specified branch.\n      description: Enables or disables checkpoint for the specified branch.\n      operationId: EnableCheckpoint\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Enabled:\n                      type: boolean\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableSave:\n    post:\n      summary: Enables or disables save for the specified branch.\n      description: Enables or disables save for the specified branch.\n      operationId: EnableSave\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Enabled:\n                      type: boolean\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/enableTag:\n    post:\n      summary: Enables or disables tag for the specified branch.\n      description: Enables or disables tag for the specified branch.\n      operationId: EnableTag\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Enabled:\n                      type: boolean\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/delete:\n    post:\n      summary: Deletes the specified branch.\n      description: Deletes the specified branch.\n      operationId: Delete\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n      responses:\n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/get:\n    post:\n      summary: Gets the specified branch.\n      description: Gets the specified branch.\n      operationId: Get\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  Class:\n                    type: string\n                    example: BranchDto\n                  BranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  BranchName:\n                    type: string\n                    example: MyBranch\n                  ParentBranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  BasedOn:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  RepositoryId:\n                    type: string\n                    format: uuid\n                    example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                  UserId:\n                    type: string\n                  PromotionEnabled:\n                    type: boolean\n                  CommitEnabled:\n                    type: boolean\n                  CheckpointEnabled:\n                    type: boolean\n                  SaveEnabled:\n                    type: boolean\n                  TagEnabled:\n                    type: boolean\n                  LatestPromotion:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestCommit:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestCheckpoint:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestSave:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  CreatedAt:\n                    type: string\n                    format: date-time\n                  UpdatedAt:\n                    type: string\n                    format: date-time\n                    nullable: true\n                  DeletedAt:\n                    type: string\n                    format: date-time\n                    nullable: true\n                required:\n                  - Class\n                  - BranchId\n                  - BranchName\n                  - ParentBranchId\n                  - BasedOn\n                  - RepositoryId\n                  - UserId\n                  - PromotionEnabled\n                  - CommitEnabled\n                  - CheckpointEnabled\n                  - SaveEnabled\n                  - TagEnabled\n                  - LatestPromotion\n                  - LatestCommit\n                  - LatestCheckpoint\n                  - LatestSave\n                  - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getParentBranch:\n    post:\n      summary: Gets the parent branch of the specified branch.\n      description: Gets the parent branch of the specified branch.\n      operationId: GetParentBranch\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  Class:\n                    type: string\n                    example: BranchDto\n                  BranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  BranchName:\n                    type: string\n                    example: MyBranch\n                  ParentBranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  BasedOn:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  RepositoryId:\n                    type: string\n                    format: uuid\n                    example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                  UserId:\n                    type: string\n                  PromotionEnabled:\n                    type: boolean\n                  CommitEnabled:\n                    type: boolean\n                  CheckpointEnabled:\n                    type: boolean\n                  SaveEnabled:\n                    type: boolean\n                  TagEnabled:\n                    type: boolean\n                  LatestPromotion:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestCommit:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestCheckpoint:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  LatestSave:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  CreatedAt:\n                    type: string\n                    format: date-time\n                  UpdatedAt:\n                    type: string\n                    format: date-time\n                    nullable: true\n                  DeletedAt:\n                    type: string\n                    format: date-time\n                    nullable: true\n                required:\n                  - Class\n                  - BranchId\n                  - BranchName\n                  - ParentBranchId\n                  - BasedOn\n                  - RepositoryId\n                  - UserId\n                  - PromotionEnabled\n                  - CommitEnabled\n                  - CheckpointEnabled\n                  - SaveEnabled\n                  - TagEnabled\n                  - LatestPromotion\n                  - LatestCommit\n                  - LatestCheckpoint\n                  - LatestSave\n                  - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getReference:\n    post:\n      summary: Gets the specified reference.\n      description: Gets the specified reference.\n      operationId: GetReference\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - allOf:\n                        - type: object\n                          properties:\n                            CorrelationId:\n                              type: string\n                              description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                            Principal:\n                              type: string\n                        - type: object\n                          properties:\n                            BranchId:\n                              type: string\n                              format: uuid\n                              example: de7bf47d-23ae-4599-af68-68a317ea390d\n                            BranchName:\n                              type: string\n                              example: MyBranch\n                            OwnerId:\n                              type: string\n                              format: uuid\n                              example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                            OwnerName:\n                              type: string\n                            OrganizationId:\n                              type: string\n                              format: uuid\n                              example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                            OrganizationName:\n                              type: string\n                            RepositoryId:\n                              type: string\n                              format: uuid\n                              example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                            RepositoryName:\n                              type: string\n                    - type: object\n                      properties:\n                        Sha256Hash:\n                          type: string\n                          example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                        ReferenceId:\n                          type: string\n                          format: uuid\n                          example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  Class:\n                    type: string\n                    example: ReferenceDto\n                  ReferenceId:\n                    type: string\n                    format: uuid\n                    example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                  BranchId:\n                    type: string\n                    format: uuid\n                    example: de7bf47d-23ae-4599-af68-68a317ea390d\n                  DirectoryId:\n                    type: string\n                    format: uuid\n                    example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                  Sha256Hash:\n                    type: string\n                    example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                  ReferenceType:\n                    type: string\n                    enum:\n                      - Promotion\n                      - Commit\n                      - Checkpoint\n                      - Save\n                      - Tag\n                    example: Commit\n                  ReferenceText:\n                    type: string\n                  CreatedAt:\n                    type: string\n                    format: date-time\n                required:\n                  - Class\n                  - ReferenceId\n                  - BranchId\n                  - DirectoryId\n                  - Sha256Hash\n                  - ReferenceType\n                  - ReferenceText\n                  - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getReferences:\n    post:\n      summary: Gets the references for the specified branch.\n      description: Gets the references for the specified branch.\n      operationId: GetReferences\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    Class:\n                      type: string\n                      example: ReferenceDto\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                    BranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceType:\n                      type: string\n                      enum:\n                        - Promotion\n                        - Commit\n                        - Checkpoint\n                        - Save\n                        - Tag\n                      example: Commit\n                    ReferenceText:\n                      type: string\n                    CreatedAt:\n                      type: string\n                      format: date-time\n                  required:\n                    - Class\n                    - ReferenceId\n                    - BranchId\n                    - DirectoryId\n                    - Sha256Hash\n                    - ReferenceType\n                    - ReferenceText\n                    - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getPromotions:\n    post:\n      summary: Gets the promotions for the specified branch.\n      description: Gets the promotions for the specified branch.\n      operationId: GetPromotions\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    Class:\n                      type: string\n                      example: ReferenceDto\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                    BranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceType:\n                      type: string\n                      enum:\n                        - Promotion\n                        - Commit\n                        - Checkpoint\n                        - Save\n                        - Tag\n                      example: Commit\n                    ReferenceText:\n                      type: string\n                    CreatedAt:\n                      type: string\n                      format: date-time\n                  required:\n                    - Class\n                    - ReferenceId\n                    - BranchId\n                    - DirectoryId\n                    - Sha256Hash\n                    - ReferenceType\n                    - ReferenceText\n                    - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getCommits:\n    post:\n      summary: Gets the commits for the specified branch.\n      description: Gets the commits for the specified branch.\n      operationId: GetCommits\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    Class:\n                      type: string\n                      example: ReferenceDto\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                    BranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceType:\n                      type: string\n                      enum:\n                        - Promotion\n                        - Commit\n                        - Checkpoint\n                        - Save\n                        - Tag\n                      example: Commit\n                    ReferenceText:\n                      type: string\n                    CreatedAt:\n                      type: string\n                      format: date-time\n                  required:\n                    - Class\n                    - ReferenceId\n                    - BranchId\n                    - DirectoryId\n                    - Sha256Hash\n                    - ReferenceType\n                    - ReferenceText\n                    - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getCheckpoints:\n    post:\n      summary: Gets the checkpoints for the specified branch.\n      description: Gets the checkpoints for the specified branch.\n      operationId: GetCheckpoints\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    Class:\n                      type: string\n                      example: ReferenceDto\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                    BranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceType:\n                      type: string\n                      enum:\n                        - Promotion\n                        - Commit\n                        - Checkpoint\n                        - Save\n                        - Tag\n                      example: Commit\n                    ReferenceText:\n                      type: string\n                    CreatedAt:\n                      type: string\n                      format: date-time\n                  required:\n                    - Class\n                    - ReferenceId\n                    - BranchId\n                    - DirectoryId\n                    - Sha256Hash\n                    - ReferenceType\n                    - ReferenceText\n                    - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getSaves:\n    post:\n      summary: Gets the saves for the specified branch.\n      description: Gets the saves for the specified branch.\n      operationId: GetSaves\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    Class:\n                      type: string\n                      example: ReferenceDto\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                    BranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceType:\n                      type: string\n                      enum:\n                        - Promotion\n                        - Commit\n                        - Checkpoint\n                        - Save\n                        - Tag\n                      example: Commit\n                    ReferenceText:\n                      type: string\n                    CreatedAt:\n                      type: string\n                      format: date-time\n                  required:\n                    - Class\n                    - ReferenceId\n                    - BranchId\n                    - DirectoryId\n                    - Sha256Hash\n                    - ReferenceType\n                    - ReferenceText\n                    - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /branch/getTags:\n    post:\n      summary: Gets the tags for the specified branch.\n      description: Gets the tags for the specified branch.\n      operationId: GetTags\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              allOf:\n                - allOf:\n                    - type: object\n                      properties:\n                        CorrelationId:\n                          type: string\n                          description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n                        Principal:\n                          type: string\n                    - type: object\n                      properties:\n                        BranchId:\n                          type: string\n                          format: uuid\n                          example: de7bf47d-23ae-4599-af68-68a317ea390d\n                        BranchName:\n                          type: string\n                          example: MyBranch\n                        OwnerId:\n                          type: string\n                          format: uuid\n                          example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n                        OwnerName:\n                          type: string\n                        OrganizationId:\n                          type: string\n                          format: uuid\n                          example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n                        OrganizationName:\n                          type: string\n                        RepositoryId:\n                          type: string\n                          format: uuid\n                          example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n                        RepositoryName:\n                          type: string\n                - type: object\n                  properties:\n                    FullSha:\n                      type: boolean\n                    MaxCount:\n                      type: integer\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    Class:\n                      type: string\n                      example: ReferenceDto\n                    ReferenceId:\n                      type: string\n                      format: uuid\n                      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n                    BranchId:\n                      type: string\n                      format: uuid\n                      example: de7bf47d-23ae-4599-af68-68a317ea390d\n                    DirectoryId:\n                      type: string\n                      format: uuid\n                      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n                    Sha256Hash:\n                      type: string\n                      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n                    ReferenceType:\n                      type: string\n                      enum:\n                        - Promotion\n                        - Commit\n                        - Checkpoint\n                        - Save\n                        - Tag\n                      example: Commit\n                    ReferenceText:\n                      type: string\n                    CreatedAt:\n                      type: string\n                      format: date-time\n                  required:\n                    - Class\n                    - ReferenceId\n                    - BranchId\n                    - DirectoryId\n                    - Sha256Hash\n                    - ReferenceType\n                    - ReferenceText\n                    - CreatedAt\n        '400':\n          description: Bad Request\n        '500':\n          description: Internal Server Error\n  /diff/populate:\n    post:\n      summary: Populates the diff actor without returning the diff.\n      description: |\n        Populates the diff actor without returning the diff. This endpoint is meant to be used when generating the diff through reacting to an event.\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PopulateParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /diff/getDiff:\n    post:\n      summary: Retrieves the contents of the diff.\n      description: |\n        Retrieves the contents of the diff between two directories.\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetDiffParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /diff/getDiffBySha256Hash:\n    post:\n      summary: Retrieves a diff taken by comparing two DirectoryVersions by Sha256Hash.\n      description: |\n        Retrieves a diff taken by comparing two DirectoryVersions identified by their SHA-256 hash.\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetDiffBySha256HashParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /directory/create:\n    post:\n      summary: Create a new directory version.\n      description: |\n        ### Validation rules\n\n        - `DirectoryVersion.DirectoryId` should be a valid and non-empty GUID.\n        - `DirectoryVersion.RepositoryId` should be a valid and non-empty GUID.\n        - The repository specified by `DirectoryVersion.RepositoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /directory/get:\n    post:\n      summary: Get a directory version.\n      description: |\n        ### Validation rules\n\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - `DirectoryId` should be a valid and non-empty GUID.\n        - The repository specified by `RepositoryId` should exist.\n        - The directory specified by `DirectoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/DirectoryVersion'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /directory/getDirectoryVersionsRecursive:\n    post:\n      summary: Get a directory version and all of its children.\n      description: |\n        ### Validation rules\n\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - `DirectoryId` should be a valid and non-empty GUID.\n        - The repository specified by `RepositoryId` should exist.\n        - The directory specified by `DirectoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: '#/components/schemas/DirectoryVersion'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /directory/getByDirectoryIds:\n    post:\n      summary: Get a list of directory versions by directory IDs.\n      description: |\n        ### Validation rules\n\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - The repository specified by `RepositoryId` should exist.\n        - The directories specified by `DirectoryIds` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetByDirectoryIdsParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: '#/components/schemas/DirectoryVersion'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /directory/getBySha256Hash:\n    post:\n      summary: Get a directory version by its SHA256 hash.\n      description: |\n        ### Validation rules\n\n        - `DirectoryId` should be a valid and non-empty GUID.\n        - `RepositoryId` should be a valid and non-empty GUID.\n        - `Sha256Hash` should not be empty.\n        - The repository specified by `RepositoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetBySha256HashParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Sha256Hash'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /directory/saveDirectoryVersions:\n    post:\n      summary: Save a list of directory versions.\n      description: |\n        ### Validation rules\n\n        - Each `DirectoryVersion` in the `DirectoryVersions` list should have valid properties:\n          - `DirectoryVersion.DirectoryId` should be a valid and non-empty GUID.\n          - `DirectoryVersion.RepositoryId` should be a valid and non-empty GUID.\n          - `DirectoryVersion.Sha256Hash` should not be empty.\n          - `DirectoryVersion.RelativePath` must not be empty.\n          - The repository specified by `DirectoryVersion.RepositoryId` should exist.\n\n        ### Errors and Problem Details\n\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SaveDirectoryVersionsParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/create:\n    post:\n      summary: Create an organization.\n      description: |\n        ### Validation rules\n        - `OwnerId` must be a valid non-empty `Guid`.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` is required and must not be empty.\n        - `OrganizationName` must be a valid Grace name.\n        - The specified `OwnerId` or `OwnerName` must exist.\n        - The specified `OrganizationId` must not already exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateOrganizationParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/setName:\n    post:\n      summary: Set the name of an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `NewName` is required and must not be empty.\n        - `NewName` must be a valid Grace name.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOrganizationNameParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/setType:\n    post:\n      summary: Set the type of an organization (Public, Private).\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `OrganizationType` is required and must be a valid organization type.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOrganizationTypeParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/setSearchVisibility:\n    post:\n      summary: Set the search visibility of an organization (Visible, Hidden).\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `SearchVisibility` is required and must be a valid search visibility option.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOrganizationSearchVisibilityParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/setDescription:\n    post:\n      summary: Set the description of an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `Description` is required and must not be empty.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOrganizationDescriptionParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/listRepositories:\n    post:\n      summary: List the repositories of an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ListRepositoriesParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: array\n                  $ref: '#/components/schemas/RepositoryDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/delete:\n    post:\n      summary: Delete an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `DeleteReason` is required and must not be empty.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/DeleteOrganizationParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/undelete:\n    post:\n      summary: Undelete a previously deleted organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/OrganizationParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /organization/get:\n    post:\n      summary: Get an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetOrganizationParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/create:\n    post:\n      summary: Create an owner.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must not be empty.\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must not be empty.\n        - `OwnerName` must be a valid Grace name.\n        - Owner with the same `OwnerId` must not already exist.\n        - Owner with the same `OwnerName` must not already exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateOwnerParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/setName:\n    post:\n      summary: Set the name of an owner.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `NewName` must not be empty.\n        - `NewName` must be a valid Grace name.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n        - Owner with the specified `NewName` must not already exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOwnerNameParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/setType:\n    post:\n      summary: Set the owner type (Public, Private).\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `OwnerType` must not be empty.\n        - `OwnerType` must be a valid owner type.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOwnerTypeParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/setSearchVisibility:\n    post:\n      summary: Set the owner search visibility (Visible, NotVisible).\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `SearchVisibility` must be a valid search visibility.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOwnerSearchVisibilityParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/setDescription:\n    post:\n      summary: Set the owner's description.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `Description` must not be empty.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetOwnerDescriptionParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/listOrganizations:\n    post:\n      summary: List the organizations for an owner.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/ListOrganizationsParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: '#/components/schemas/OrganizationDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/delete:\n    post:\n      summary: Delete an owner.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `DeleteReason` must not be empty.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/DeleteOwnerParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/undelete:\n    post:\n      summary: Undelete a previously-deleted owner.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/OwnerParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /owner/get:\n    post:\n      summary: Get an owner.\n      description: |\n        ### Validation rules\n\n        - `OwnerId` must be a valid GUID.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetOwnerParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OwnerDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/create:\n    post:\n      summary: Create a new repository.\n      description: |\n        This endpoint creates a new repository.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must not be empty.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The `RepositoryName` must not be empty.\n        - The `RepositoryName` must be a valid Grace name.\n        - The `OwnerId` must correspond to an existing owner.\n        - The `OrganizationId` must correspond to an existing organization.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateRepositoryParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setVisibility:\n    post:\n      summary: Sets the search visibility of the repository.\n      description: |\n        This endpoint sets the search visibility of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `Visibility` value must be valid.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetRepositoryVisibilityParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setSaveDays:\n    post:\n      summary: Sets the number of days to keep saves in the repository.\n      description: |\n        This endpoint sets the number of days to keep saves in a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `SaveDays` value must be valid.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetSaveDaysParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setCheckpointDays:\n    post:\n      summary: Sets the number of days to keep checkpoints in the repository.\n      description: |\n        This endpoint sets the number of days to keep checkpoints in a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `CheckpointDays` value must be valid.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetCheckpointDaysParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setStatus:\n    post:\n      summary: Sets the status of the repository (Public, Private).\n      description: |\n        This endpoint sets the status of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `Status` value must be a valid repository status.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetRepositoryStatusParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setDefaultServerApiVersion:\n    post:\n      summary: Sets the default server API version for the repository.\n      description: |\n        This endpoint sets the default server API version for a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `DefaultServerApiVersion` must not be empty.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetDefaultServerApiVersionParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setRecordSaves:\n    post:\n      summary: Sets whether or not to keep saves in the repository.\n      description: |\n        This endpoint sets whether or not to keep saves in a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RecordSavesParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/setDescription:\n    post:\n      summary: Sets the description of the repository.\n      description: |\n        This endpoint sets the description of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `Description` must not be empty.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/SetRepositoryDescriptionParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/delete:\n    post:\n      summary: Deletes the repository.\n      description: |\n        This endpoint deletes a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `DeleteReason` must not be empty.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/DeleteRepositoryParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/undelete:\n    post:\n      summary: Undeletes a previously-deleted repository.\n      description: |\n        This endpoint undeletes a previously-deleted repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The repository must exist.\n        - The repository must be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RepositoryParameters'\n      responses:\n        '200':\n          $ref: '#/components/responses/200'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/exists:\n    post:\n      summary: Checks if a repository exists with the given parameters.\n      description: |\n        This endpoint checks if a repository exists based on the provided parameters.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RepositoryParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: boolean\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/isEmpty:\n    post:\n      summary: Checks if a repository is empty.\n      description: |\n        This endpoint checks if a repository is empty, meaning it has just been created and has no data.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RepositoryParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: boolean\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/get:\n    post:\n      summary: Gets a repository.\n      description: |\n        This endpoint retrieves the details of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The `RepositoryName` must not be empty.\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/RepositoryParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RepositoryDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/getBranches:\n    post:\n      summary: Gets a repository's branches.\n      description: |\n        This endpoint retrieves the branches of a repository.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `MaxCount` must be a number between 1 and 1000 (inclusive).\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetBranchesParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: '#/components/schemas/BranchDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/getReferencesByReferenceId:\n    post:\n      summary: Gets a list of references by reference IDs.\n      description: |\n        This endpoint retrieves a list of references based on the provided reference IDs.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `ReferenceIds` must not be empty.\n        - The `MaxCount` must be a number between 1 and 1000 (inclusive).\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetReferencesByReferenceIdParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: '#/components/schemas/ReferenceDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\n  /repository/getBranchesByBranchId:\n    post:\n      summary: Gets a list of branches by branch IDs.\n      description: |\n        This endpoint retrieves a list of branches based on the provided branch IDs.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `BranchIds` must not be empty.\n        - The `MaxCount` must be a number between 1 and 1000 (inclusive).\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GetBranchesByBranchIdParameters'\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: '#/components/schemas/BranchDto'\n        '400':\n          $ref: '#/components/responses/400'\n        '500':\n          $ref: '#/components/responses/500'\ncomponents:\n  schemas:\n    '200':\n      description: OK\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/GraceReturnValue'\n    BranchParameters:\n      description: Parameters for many endpoints in the /branch path.\n      type: object\n      properties:\n        ownerId:\n          type: string\n        ownerName:\n          $ref: '#/components/schemas/OwnerName'\n        organizationId:\n          type: string\n        organizationName:\n          $ref: '#/components/schemas/OrganizationName'\n        repositoryId:\n          type: string\n        repositoryName:\n          $ref: '#/components/schemas/RepositoryName'\n        branchId:\n          type: string\n        branchName:\n          $ref: '#/components/schemas/BranchName'\n    BranchQueryParameters:\n      description: Base class for parameters for branch queries.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            sha256Hash:\n              $ref: '#/components/schemas/Sha256Hash'\n            referenceId:\n              type: string\n    CreateBranchParameters:\n      description: Parameters for the /branch/create endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            parentBranchId:\n              type: string\n            parentBranchName:\n              $ref: '#/components/schemas/BranchName'\n    RebaseParameters:\n      description: Parameters for the /branch/rebase endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            basedOn:\n              $ref: '#/components/schemas/ReferenceId'\n    CreateReferenceParameters:\n      description: Parameters for the various /branch/create[reference] endpoints.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            directoryId:\n              $ref: '#/components/schemas/DirectoryId'\n            sha256Hash:\n              $ref: '#/components/schemas/Sha256Hash'\n            message:\n              type: string\n    SetBranchNameParameters:\n      description: Parameters for the /branch/setName endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            newName:\n              type: string\n    EnableFeatureParameters:\n      description: Parameters for the various /branch/enable[feature] endpoints.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            enabled:\n              type: boolean\n    DeleteBranchParameters:\n      description: Parameters for the /branch/delete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n    GetReferenceParameters:\n      description: Parameters for the /branch/getReference endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchQueryParameters'\n    GetReferencesParameters:\n      description: Parameters for the /branch/getReferences and /branch/get[reference] endpoints.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            fullSha:\n              type: boolean\n            maxCount:\n              type: integer\n    GetDiffsForReferenceTypeParameters:\n      description: Parameters for the /branch/getDiffsForReferenceType endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            referenceType:\n              type: string\n            maxCount:\n              type: integer\n    GetDiffsForReferencesParameters:\n      description: Parameters for the /branch/getDiffsForReferences endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchParameters'\n        - type: object\n          properties:\n            references:\n              type: string\n            maxCount:\n              type: integer\n    GetBranchParameters:\n      description: Parameters for the /branch/get endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchQueryParameters'\n    SwitchParameters:\n      description: Parameters for the /branch/switch endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchQueryParameters'\n    GetBranchVersionParameters:\n      description: Parameters for the /branch/getVersion endpoint.\n      allOf:\n        - $ref: '#/components/schemas/BranchQueryParameters'\n    DiffParameters:\n      description: Parameters for diff endpoints.\n      allOf:\n        - $ref: '#/components/schemas/CommonParameters'\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          $ref: '#/components/schemas/OwnerName'\n        OrganizationId:\n          type: string\n        OrganizationName:\n          $ref: '#/components/schemas/OrganizationName'\n        RepositoryId:\n          type: string\n        RepositoryName:\n          $ref: '#/components/schemas/RepositoryName'\n        DirectoryId1:\n          $ref: '#/components/schemas/DirectoryId'\n        DirectoryId2:\n          $ref: '#/components/schemas/DirectoryId'\n    PopulateParameters:\n      description: Parameters for diff population.\n      allOf:\n        - $ref: '#/components/schemas/DiffParameters'\n    GetDiffParameters:\n      description: Parameters for retrieving a diff.\n      allOf:\n        - $ref: '#/components/schemas/DiffParameters'\n    GetDiffByReferenceTypeParameters:\n      description: Parameters for retrieving a diff by reference type.\n      allOf:\n        - $ref: '#/components/schemas/DiffParameters'\n      properties:\n        BranchId:\n          type: string\n        BranchName:\n          $ref: '#/components/schemas/BranchName'\n    GetDiffBySha256HashParameters:\n      description: Parameters for retrieving a diff by SHA256 hash.\n      allOf:\n        - $ref: '#/components/schemas/DiffParameters'\n      properties:\n        Sha256Hash1:\n          $ref: '#/components/schemas/Sha256Hash'\n        Sha256Hash2:\n          $ref: '#/components/schemas/Sha256Hash'\n    DirectoryParameters:\n      description: Parameters for many endpoints in the /directory path.\n      type: object\n      properties:\n        DirectoryId:\n          type: string\n          description: Unique identifier of the directory.\n      allOf:\n        - $ref: '#/components/schemas/CommonParameters'\n    CreateParameters:\n      description: Parameters for the /directory/create endpoint.\n      type: object\n      properties:\n        DirectoryVersion:\n          $ref: '#/components/schemas/DirectoryVersion'\n      allOf:\n        - $ref: '#/components/schemas/DirectoryParameters'\n    GetParameters:\n      description: Parameters for the /directory/get endpoint.\n      type: object\n      properties:\n        RepositoryId:\n          type: string\n          description: Unique identifier of the repository.\n      allOf:\n        - $ref: '#/components/schemas/DirectoryParameters'\n    GetByDirectoryIdsParameters:\n      description: Parameters for the /directory/getByDirectoryIds endpoint.\n      type: object\n      properties:\n        RepositoryId:\n          type: string\n          description: Unique identifier of the repository.\n        DirectoryIds:\n          type: array\n          items:\n            $ref: '#/components/schemas/DirectoryId'\n      allOf:\n        - $ref: '#/components/schemas/DirectoryParameters'\n    GetBySha256HashParameters:\n      description: Parameters for the /directory/getBySha256Hash endpoint.\n      type: object\n      properties:\n        RepositoryId:\n          type: string\n          description: Unique identifier of the repository.\n        Sha256Hash:\n          type: string\n          description: The SHA256 hash value.\n      allOf:\n        - $ref: '#/components/schemas/DirectoryParameters'\n    SaveDirectoryVersionsParameters:\n      description: Parameters for the /directory/saveDirectoryVersions endpoint.\n      type: object\n      properties:\n        DirectoryVersions:\n          type: array\n          items:\n            $ref: '#/components/schemas/DirectoryVersion'\n      allOf:\n        - $ref: '#/components/schemas/DirectoryParameters'\n    BranchDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        BranchId:\n          $ref: '#/components/schemas/BranchId'\n        BranchName:\n          $ref: '#/components/schemas/BranchName'\n        ParentBranchId:\n          $ref: '#/components/schemas/BranchId'\n        BasedOn:\n          $ref: '#/components/schemas/ReferenceId'\n        RepositoryId:\n          $ref: '#/components/schemas/RepositoryId'\n        UserId:\n          $ref: '#/components/schemas/UserId'\n        PromotionEnabled:\n          type: boolean\n        CommitEnabled:\n          type: boolean\n        CheckpointEnabled:\n          type: boolean\n        SaveEnabled:\n          type: boolean\n        TagEnabled:\n          type: boolean\n        LatestPromotion:\n          $ref: '#/components/schemas/ReferenceId'\n        LatestCommit:\n          $ref: '#/components/schemas/ReferenceId'\n        LatestCheckpoint:\n          $ref: '#/components/schemas/ReferenceId'\n        LatestSave:\n          $ref: '#/components/schemas/ReferenceId'\n        CreatedAt:\n          $ref: '#/components/schemas/Instant'\n        UpdatedAt:\n          type: object\n          properties:\n            value:\n              $ref: '#/components/schemas/Instant'\n        DeletedAt:\n          type: object\n          properties:\n            value:\n              $ref: '#/components/schemas/Instant'\n    DiffDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        HasDifferences:\n          type: boolean\n        DirectoryId1:\n          $ref: '#/components/schemas/DirectoryId'\n        Directory1CreatedAt:\n          $ref: '#/components/schemas/Instant'\n        DirectoryId2:\n          $ref: '#/components/schemas/DirectoryId'\n        Directory2CreatedAt:\n          $ref: '#/components/schemas/Instant'\n        Differences:\n          type: array\n          items:\n            $ref: '#/components/schemas/FileSystemDifference'\n        FileDiffs:\n          type: array\n          items:\n            $ref: '#/components/schemas/FileDiff'\n    OrganizationDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        OrganizationId:\n          $ref: '#/components/schemas/OrganizationId'\n        OrganizationName:\n          $ref: '#/components/schemas/OrganizationName'\n        OwnerId:\n          $ref: '#/components/schemas/OwnerId'\n        OrganizationType:\n          $ref: '#/components/schemas/OrganizationType'\n        Description:\n          type: string\n        SearchVisibility:\n          $ref: '#/components/schemas/SearchVisibility'\n        Repositories:\n          type: object\n          additionalProperties:\n            $ref: '#/components/schemas/RepositoryName'\n        CreatedAt:\n          $ref: '#/components/schemas/Instant'\n        UpdatedAt:\n          type: object\n          properties:\n            value:\n              $ref: '#/components/schemas/Instant'\n        DeletedAt:\n          type: object\n          properties:\n            value:\n              $ref: '#/components/schemas/Instant'\n        DeleteReason:\n          type: string\n    OwnerDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        OwnerId:\n          $ref: '#/components/schemas/OwnerId'\n        OwnerName:\n          $ref: '#/components/schemas/OwnerName'\n        OwnerType:\n          $ref: '#/components/schemas/OwnerType'\n        Description:\n          type: string\n        SearchVisibility:\n          $ref: '#/components/schemas/SearchVisibility'\n        Organizations:\n          type: object\n          additionalProperties:\n            $ref: '#/components/schemas/OrganizationName'\n        CreatedAt:\n          $ref: '#/components/schemas/Instant'\n        UpdatedAt:\n          type: object\n          properties:\n            value:\n              $ref: '#/components/schemas/Instant'\n        DeletedAt:\n          type: object\n          properties:\n            value:\n              $ref: '#/components/schemas/Instant'\n        DeleteReason:\n          type: string\n    ReferenceDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        ReferenceId:\n          $ref: '#/components/schemas/ReferenceId'\n        BranchId:\n          $ref: '#/components/schemas/BranchId'\n        DirectoryId:\n          $ref: '#/components/schemas/DirectoryId'\n        Sha256Hash:\n          $ref: '#/components/schemas/Sha256Hash'\n        ReferenceType:\n          $ref: '#/components/schemas/ReferenceType'\n        ReferenceText:\n          $ref: '#/components/schemas/ReferenceText'\n        CreatedAt:\n          $ref: '#/components/schemas/Instant'\n    RepositoryDto:\n      description: (automatically generated)\n      type: object\n      properties:\n        Class:\n          type: string\n        RepositoryId:\n          $ref: '#/components/schemas/RepositoryId'\n        OwnerId:\n          $ref: '#/components/schemas/OwnerId'\n        OrganizationId:\n          $ref: '#/components/schemas/OrganizationId'\n        RepositoryName:\n          $ref: '#/components/schemas/RepositoryName'\n        ObjectStorageProvider:\n          $ref: '#/components/schemas/ObjectStorageProvider'\n        StorageAccountName:\n          type: string\n        StorageContainerName:\n          $ref: '#/components/schemas/StorageContainerName'\n        RepositoryVisibility:\n          $ref: '#/components/schemas/RepositoryVisibility'\n        RepositoryStatus:\n          $ref: '#/components/schemas/RepositoryStatus'\n        Branches:\n          type: array\n          items:\n            $ref: '#/components/schemas/BranchName'\n        DefaultServerApiVersion:\n          type: string\n        DefaultBranchName:\n          $ref: '#/components/schemas/BranchName'\n        SaveDays:\n          type: number\n          format: double\n        CheckpointDays:\n          type: number\n    OrganizationParameters:\n      description: Parameters for many endpoints in the /organization path.\n      allOf:\n        - $ref: '#/components/schemas/CommonParameters'\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          type: string\n        OrganizationId:\n          type: string\n        OrganizationName:\n          type: string\n    CreateOrganizationParameters:\n      description: Parameters for the /organization/create endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n    SetOrganizationNameParameters:\n      description: Parameters for the /organization/setName endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n      properties:\n        NewName:\n          type: string\n    SetOrganizationTypeParameters:\n      description: Parameters for the /organization/setType endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n      properties:\n        OrganizationType:\n          type: string\n    SetOrganizationSearchVisibilityParameters:\n      description: Parameters for the /organization/setSearchVisibility endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n      properties:\n        SearchVisibility:\n          type: string\n    SetOrganizationDescriptionParameters:\n      description: Parameters for the /organization/setDescription endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n      properties:\n        Description:\n          type: string\n    DeleteOrganizationParameters:\n      description: Parameters for the /organization/delete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n      properties:\n        Force:\n          type: boolean\n        DeleteReason:\n          type: string\n    UndeleteOrganizationParameters:\n      description: Parameters for the /organization/undelete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n    GetOrganizationParameters:\n      description: Parameters for the /organization/get endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n    ListRepositoriesParameters:\n      description: Parameters for the /organization/listRepositories endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OrganizationParameters'\n    OwnerParameters:\n      description: Parameters for many endpoints in the /owner path.\n      allOf:\n        - $ref: '#/components/schemas/CommonParameters'\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          type: string\n    CreateOwnerParameters:\n      description: Parameters for the /owner/create endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n    SetOwnerNameParameters:\n      description: Parameters for the /owner/setName endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n      properties:\n        NewName:\n          type: string\n    SetOwnerTypeParameters:\n      description: Parameters for the /owner/setType endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n      properties:\n        OwnerType:\n          type: string\n    SetOwnerSearchVisibilityParameters:\n      description: Parameters for the /owner/setSearchVisibility endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n      properties:\n        SearchVisibility:\n          type: string\n    SetOwnerDescriptionParameters:\n      description: Parameters for the /owner/setDescription endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n      properties:\n        Description:\n          type: string\n    DeleteOwnerParameters:\n      description: Parameters for the /owner/delete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n      properties:\n        Force:\n          type: boolean\n        DeleteReason:\n          type: string\n    UndeleteOwnerParameters:\n      description: Parameters for the /owner/undelete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n    GetOwnerParameters:\n      description: Parameters for the /owner/get endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n    ListOrganizationsParameters:\n      description: Parameters for the /owner/listOrganizations endpoint.\n      allOf:\n        - $ref: '#/components/schemas/OwnerParameters'\n    ReferenceParameters:\n      description: Parameters for many endpoints in the /reference path.\n      type: object\n      properties:\n        ReferenceId:\n          type: string\n          description: Unique identifier of the reference.\n        ReferenceType:\n          type: string\n          description: Type of the reference.\n        RepositoryText:\n          type: string\n          description: Textual representation of the repository.\n        RepositoryName:\n          type: string\n          description: Name of the repository.\n      allOf:\n        - $ref: '#/components/schemas/CommonParameters'\n    RepositoryParameters:\n      description: Parameters for many endpoints in the /repository path.\n      type: object\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          type: string\n        OrganizationId:\n          type: string\n        OrganizationName:\n          type: string\n        RepositoryId:\n          type: string\n        RepositoryName:\n          type: string\n    CreateRepositoryParameters:\n      description: Parameters for the /repository/create endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n    IsEmptyParameters:\n      description: Parameters for the /repository/isEmpty endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n    InitParameters:\n      description: Parameters for the /repository/init endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        GraceConfig:\n          type: string\n    SetRepositoryVisibilityParameters:\n      description: Parameters for the /repository/setVisibility endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        Visibility:\n          type: string\n    SetRepositoryStatusParameters:\n      description: Parameters for the /repository/setStatus endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        Status:\n          type: string\n    RecordSavesParameters:\n      description: Parameters for the /repository/recordSaves endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        RecordSaves:\n          type: boolean\n    SetSaveDaysParameters:\n      description: Parameters for the /repository/setSaveDays endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        SaveDays:\n          type: number\n    SetCheckpointDaysParameters:\n      description: Parameters for the /repository/setCheckpointDays\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        CheckpointDays:\n          type: number\n    SetRepositoryDescriptionParameters:\n      description: Parameters for the /repository/setDescription endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        Description:\n          type: string\n    SetDefaultServerApiVersionParameters:\n      description: Parameters for the /repository/setDefaultServerApiVersion endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        DefaultServerApiVersion:\n          type: string\n    SetRepositoryNameParameters:\n      description: Parameters for the /repository/setName endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        NewName:\n          type: string\n    DeleteRepositoryParameters:\n      description: Parameters for the /repository/delete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        Force:\n          type: boolean\n        DeleteReason:\n          type: string\n    EnablePromotionTypeParameters:\n      description: Parameters for enabling promotion type.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        Enabled:\n          type: boolean\n    GetReferencesByReferenceIdParameters:\n      description: Parameters for the /repository/getReferencesByReferenceId endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        ReferenceIds:\n          type: array\n          items:\n            $ref: '#/components/schemas/ReferenceId'\n        MaxCount:\n          type: integer\n    GetBranchesParameters:\n      description: Parameters for the /repository/getBranched endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        IncludeDeleted:\n          type: boolean\n        MaxCount:\n          type: integer\n    GetBranchesByBranchIdParameters:\n      description: Parameters for the /repository/getBranchesByBranchId endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n      properties:\n        BranchIds:\n          type: array\n          items:\n            $ref: '#/components/schemas/BranchId'\n        MaxCount:\n          type: integer\n        IncludeDeleted:\n          type: boolean\n    UndeleteRepositoryParameters:\n      description: Parameters for the /repository/undelete endpoint.\n      allOf:\n        - $ref: '#/components/schemas/RepositoryParameters'\n    Instant:\n      type: string\n      format: date-time\n    ProblemDetails:\n      type: object\n      properties:\n        type:\n          type: string\n          description: A URI reference that identifies the problem type.\n        title:\n          type: string\n          description: A short, human-readable summary of the problem type.\n        status:\n          type: integer\n          description: The HTTP status code generated by the origin server for this occurrence of the problem.\n        detail:\n          type: string\n          description: A human-readable explanation specific to this occurrence of the problem.\n        instance:\n          type: string\n          description: A URI reference that identifies the specific occurrence of the problem.\n      required:\n        - type\n        - title\n        - status\n        - detail\n        - instance\n    BranchId:\n      type: string\n      format: uuid\n      example: de7bf47d-23ae-4599-af68-68a317ea390d\n    BranchName:\n      type: string\n      example: MyBranch\n    CorrelationId:\n      type: string\n      description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n    ContainerName:\n      type: string\n      description: The name of the container must be a valid Grace name. (See the Grace documentation for more information.)\n    DirectoryId:\n      type: string\n      format: uuid\n      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n    FilePath:\n      type: string\n    GraceIgnoreEntry:\n      type: string\n    OrganizationId:\n      type: string\n      format: uuid\n      example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n    OrganizationName:\n      type: string\n    OwnerId:\n      type: string\n      format: uuid\n      example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n    OwnerName:\n      type: string\n    ReferenceId:\n      type: string\n      format: uuid\n      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n    ReferenceName:\n      type: string\n    ReferenceText:\n      type: string\n    RepositoryId:\n      type: string\n      format: uuid\n      example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n    RepositoryName:\n      type: string\n    RelativePath:\n      type: string\n    Sha256Hash:\n      type: string\n      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n    StorageAccountName:\n      type: string\n    StorageConnectionString:\n      type: string\n    StorageContainerName:\n      type: string\n    UriWithSharedAccessSignature:\n      type: string\n    UserId:\n      type: string\n    OwnerType:\n      type: string\n      enum:\n        - Public\n        - Private\n      example: Public\n    OrganizationType:\n      type: string\n      enum:\n        - Public\n        - Private\n      summary: The OrganizationType determines whether the Organization is visible to the public or not.\n      description: The OrganizationType determines whether the Organization is visible to the public or not.\n      example: Public\n    SearchVisibility:\n      type: string\n      enum:\n        - Visible\n        - NotVisible\n      example: NotVisible\n    ReferenceType:\n      type: string\n      enum:\n        - Promotion\n        - Commit\n        - Checkpoint\n        - Save\n        - Tag\n      example: Commit\n    CommonParameters:\n      description: Common parameters that are used across (almost) every endpoint in Grace.\n      type: object\n      properties:\n        CorrelationId:\n          type: string\n          description: A unique identifier for correlating logs and telemetry.\n        Principal:\n          type: string\n          description: The entity on whose behalf the action is being performed.\n    FileVersion:\n      type: object\n      properties:\n        Class:\n          type: string\n        RepositoryId:\n          - $ref: '#/components/schemas/RepositoryId'\n        RelativePath:\n          - $ref: '#/components/schemas/RelativePath'\n        Sha256Hash:\n          - $ref: '#/components/schemas/Sha256Hash'\n        IsBinary:\n          type: boolean\n        Size:\n          type: integer\n          format: int64\n        CreatedAt:\n          type: string\n          format: date-time\n        BlobUri:\n          type: string\n    DirectoryVersion:\n      type: object\n      properties:\n        Class:\n          type: string\n        DirectoryId:\n          - $ref: '#/components/schemas/DirectoryId'\n        RepositoryId:\n          - $ref: '#/components/schemas/RepositoryId'\n        RelativePath:\n          - $ref: '#/components/schemas/RelativePath'\n        Sha256Hash:\n          - $ref: '#/components/schemas/Sha256Hash'\n        Directories:\n          type: array\n          items:\n            - $ref: '#/components/schemas/DirectoryId'\n        Files:\n          type: array\n          items:\n            - $ref: '#/components/schemas/FileVersion'\n        Size:\n          type: integer\n          format: int64\n        RecursiveSize:\n          type: integer\n          format: int64\n        CreatedAt:\n          type: string\n          format: date-time\n    EventMetadata:\n      description: null\n      type: object\n      properties:\n        Timestamp:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Principal:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n    GraceReturnValue:\n      type: object\n      properties:\n        ReturnValue:\n          type: object\n        EventTime:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n    GraceError:\n      type: object\n      properties:\n        Error:\n          type: string\n        EventTime:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n    GraceResult:\n      oneOf:\n        - - $ref: '#/components/schemas/GraceReturnValue'\n        - - $ref: '#/components/schemas/GraceError'\n    FileSystemEntryType:\n      type: string\n      enum:\n        - Directory\n        - File\n    DifferenceType:\n      type: string\n      enum:\n        - Add\n        - Change\n        - Delete\n    FileSystemDifference:\n      type: object\n      properties:\n        DifferenceType:\n          $ref: null\n        FileSystemEntryType:\n          $ref: null\n        RelativePath:\n          $ref: null\n      required:\n        - DifferenceType\n        - FileSystemEntryType\n        - RelativePath\n    FileDiff:\n      type: object\n      properties:\n        RelativePath:\n          $ref: null\n        FileSha1:\n          $ref: null\n        CreatedAt1:\n          $ref: null\n        FileSha2:\n          $ref: null\n        CreatedAt2:\n          $ref: null\n        IsBinary:\n          type: boolean\n        InlineDiff:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: null\n        SideBySideOld:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: null\n        SideBySideNew:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: null\n      required:\n        - RelativePath\n        - FileSha1\n        - CreatedAt1\n        - FileSha2\n        - CreatedAt2\n        - IsBinary\n        - InlineDiff\n        - SideBySideOld\n        - SideBySideNew\n    DiffPiece:\n      type: object\n      properties:\n        Position:\n          type: integer\n          nullable: true\n        SubPieces:\n          type: array\n          items:\n            $ref: null\n        Text:\n          type: string\n        Type:\n          $ref: null\n      required:\n        - Text\n        - Type\n    ChangeType:\n      type: integer\n      enum:\n        - 0\n        - 1\n        - 2\n        - 3\n        - 4\n      enumNames:\n        - Unchanged\n        - Deleted\n        - Inserted\n        - Imaginary\n        - Modified\n    ObjectStorageProvider:\n      type: string\n      enum:\n        - AWSS3\n        - AzureBlobStorage\n        - GoogleCloudStorage\n        - Unknown\n    RepositoryVisibility:\n      type: string\n      enum:\n        - Private\n        - Public\n    RepositoryStatus:\n      type: string\n      enum:\n        - Active\n        - Suspended\n        - Closed\n        - Deleted\n  securitySchemes:\n    api_key:\n      type: apiKey\n      name: X-API-Key\n      in: header\n    bearerAuth:\n      type: http\n      scheme: bearer\n      bearerFormat: JWT\n  responses:\n    '200':\n      description: OK\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/GraceReturnValue'\n    '400':\n      description: Bad Request\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ProblemDetails'\n    '500':\n      description: Internal Server Error\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/ProblemDetails'\n"
  },
  {
    "path": "src/OpenAPI/Main.OpenAPI.yaml",
    "content": "openapi: 3.1.0\ninfo:\n  title: Grace Server API\n  description: >-\n    ## Grace Server API\n\n    This OpenAPI specification describes the Web API for Grace Server.\n\n    ---\n\n    ## Helpful hints\n\n    * In general, when the parameters ask for a _somethingId_ and a _somethingName_, like, OwnerId and OwnerName, or OrganizationId and OrganizationName, etc. you only need to give one or the other.\n\n    * If you have the Id's available just as readily as the names, you'll get better performance by using the Id's. It saves a lookup - whether in-memory cache, or database call - at the server.\n\n    * If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's in Grace are just strings, and feel free to send any other kind of CorrelationId you prefer.\n\n    * A valid name in Grace has between 2 and 64 characters, has a letter for the first character `^[A-Za-z]`, and letters, numbers, or a dash (-) for the rest `[A-Za-z0-9\\-_]{1,63}`.\n\n    * The full regex for valid Grace names is: `^[A-Za-z][A-Za-z0-9\\-]{1,63}$`. The ChatGPT explanation of it can be found [here](https://chat.openai.com/share/1d18c634-45ed-4ef5-bd8e-93391f74b637).\n\n    * If you notice a difference between the OpenAPI spec and the actual API, please file an issue so we can fix it. It's absolutely our intention to keep the spec and the API in sync.\n\n    ---\n\n    Please let us know if you have any questions or comments @ [Discussions](https://github.com/scottarbeit/grace/discussions) in the [Grace repository](https://github.com/scottarbeit/grace).\n  contact:\n    name: Scott Arbeit\n    url: https://twitter.com/scottarbeit\n    email: scott.arbeit@outlook.com\n  license:\n    name: MIT\n    url: https://opensource.org/licenses/MIT\n  version: \"0.1\"\nservers:\n  - url: \"http://localhost:5000\"\n    description: \"Local development server\"\nsecurity:\n  - bearerAuth: []\npaths:\n  /openApi:\n    get: \n      summary: Get the OpenAPI specification for the Grace Server API\n      description: This endpoint returns the OpenAPI specification for the Grace Server API. The specification is generated from the OpenAPI specification files in the src/OpenAPI folder.\n      operationId: GetOpenApi\n      responses: \n        '200':\n          description: OK\n        '400':\n          description: Bad Request\n  /branch/create:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1create'\n  /branch/rebase:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1rebase'\n  /branch/promote:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1promote'\n  /branch/commit:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1commit'\n  /branch/checkpoint:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1checkpoint'\n  /branch/save:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1save'\n  /branch/tag:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1tag'\n  /branch/enablePromotion:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1enablePromotion'\n  /branch/enableCommit:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1enableCommit'\n  /branch/enableCheckpoint:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1enableCheckpoint'\n  /branch/enableSave:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1enableSave'\n  /branch/enableTag:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1enableTag'\n  /branch/delete:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1delete'\n  /branch/get:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1get'\n  /branch/getParentBranch:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getParentBranch'\n  /branch/getReference:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getReference'\n  /branch/getReferences:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getReferences'\n  /branch/getPromotions:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getPromotions'\n  /branch/getCommits:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getCommits'\n  /branch/getCheckpoints:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getCheckpoints'\n  /branch/getSaves:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getSaves'\n  /branch/getTags:\n    $ref: './Branch.Paths.OpenAPI.yaml#/~1branch~1getTags'\n\n\n  /diff/populate:\n    $ref: './Diff.Paths.OpenAPI.yaml#/~1diff~1populate'\n  /diff/getDiff:\n    $ref: './Diff.Paths.OpenAPI.yaml#/~1diff~1getDiff'\n  /diff/getDiffBySha256Hash:\n    $ref: './Diff.Paths.OpenAPI.yaml#/~1diff~1getDiffBySha256Hash'\n  \n  /directory/create:\n    $ref: './Directory.Paths.OpenAPI.yaml#/~1directory~1create'\n  /directory/get:\n    $ref: './Directory.Paths.OpenAPI.yaml#/~1directory~1get'\n  /directory/getDirectoryVersionsRecursive:\n    $ref: './Directory.Paths.OpenAPI.yaml#/~1directory~1getDirectoryVersionsRecursive'\n  /directory/getByDirectoryIds:\n    $ref: './Directory.Paths.OpenAPI.yaml#/~1directory~1getByDirectoryIds'\n  /directory/getBySha256Hash:\n    $ref: './Directory.Paths.OpenAPI.yaml#/~1directory~1getBySha256Hash'\n  /directory/saveDirectoryVersions:\n    $ref: './Directory.Paths.OpenAPI.yaml#/~1directory~1saveDirectoryVersions'\n      \n  /organization/create:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1create'\n  /organization/setName:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1setName'\n  /organization/setType:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1setType'\n  /organization/setSearchVisibility:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1setSearchVisibility'\n  /organization/setDescription:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1setDescription'\n  /organization/listRepositories:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1listRepositories'\n  /organization/delete:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1delete'\n  /organization/undelete:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1undelete'\n  /organization/get:\n    $ref: './Organization.Paths.OpenAPI.yaml#/~1organization~1get'\n  \n  /owner/create:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1create'\n  /owner/setName:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1setName'\n  /owner/setType:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1setType'\n  /owner/setSearchVisibility:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1setSearchVisibility'\n  /owner/setDescription:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1setDescription'\n  /owner/listOrganizations:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1listOrganizations'\n  /owner/delete:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1delete'\n  /owner/undelete:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1undelete'\n  /owner/get:\n    $ref: './Owner.Paths.OpenAPI.yaml#/~1owner~1get'\n\n  /repository/create:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1create'\n  /repository/setVisibility:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setVisibility'\n  /repository/setSaveDays:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setSaveDays'\n  /repository/setCheckpointDays:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setCheckpointDays'\n  /repository/setStatus:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setStatus'\n  /repository/setDefaultServerApiVersion:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setDefaultServerApiVersion'\n  /repository/setRecordSaves:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setRecordSaves'\n  /repository/setDescription:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1setDescription'\n  /repository/delete:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1delete'\n  /repository/undelete:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1undelete'\n  /repository/exists:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1exists'\n  /repository/isEmpty:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1isEmpty'\n  /repository/get:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1get'\n  /repository/getBranches:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1getBranches'\n  /repository/getReferencesByReferenceId:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1getReferencesByReferenceId'\n  /repository/getBranchesByBranchId:\n    $ref: './Repository.Paths.OpenAPI.yaml#/~1repository~1getBranchesByBranchId'\n\ncomponents:\n  schemas:\n    BranchParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/BranchParameters'\n    BranchQueryParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/BranchQueryParameters'\n    CreateBranchParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/CreateBranchParameters'\n    RebaseParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/RebaseParameters'\n    CreateReferenceParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/CreateReferenceParameters'\n    SetBranchNameParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/SetBranchNameParameters'\n    EnableFeatureParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/EnableFeatureParameters'\n    DeleteBranchParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/DeleteBranchParameters'\n    GetReferenceParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/GetReferenceParameters'\n    GetReferencesParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/GetReferencesParameters'\n    GetDiffsForReferenceTypeParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/GetDiffsForReferenceTypeParameters'\n    GetDiffsForReferencesParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/GetDiffsForReferencesParameters'\n    GetBranchParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/GetBranchParameters'\n    SwitchParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/SwitchParameters'\n    GetBranchVersionParameters:\n      $ref: 'Branch.Components.OpenAPI.yaml#/GetBranchVersionParameters'\n\n    DiffParameters:\n      $ref: 'Diff.Components.OpenAPI.yaml#/DiffParameters'\n    PopulateParameters:\n      $ref: 'Diff.Components.OpenAPI.yaml#/PopulateParameters'\n    GetDiffParameters:\n      $ref: 'Diff.Components.OpenAPI.yaml#/GetDiffParameters'\n    GetDiffByReferenceTypeParameters:\n      $ref: 'Diff.Components.OpenAPI.yaml#/GetDiffByReferenceTypeParameters'\n    GetDiffBySha256HashParameters:\n      $ref: 'Diff.Components.OpenAPI.yaml#/GetDiffBySha256HashParameters'\n    \n    DirectoryParameters:\n      $ref: 'Directory.Components.OpenAPI.yaml#/DirectoryParameters'\n    CreateParameters:\n      $ref: 'Directory.Components.OpenAPI.yaml#/CreateParameters'\n    GetParameters:\n      $ref: 'Directory.Components.OpenAPI.yaml#/GetParameters'\n    GetByDirectoryIdsParameters:\n      $ref: 'Directory.Components.OpenAPI.yaml#/GetByDirectoryIdsParameters'\n    GetBySha256HashParameters:\n      $ref: 'Directory.Components.OpenAPI.yaml#/GetBySha256HashParameters'\n    SaveDirectoryVersionsParameters:\n      $ref: 'Directory.Components.OpenAPI.yaml#/SaveDirectoryVersionsParameters'\n\n    BranchDto:\n      $ref: 'Dto.Components.OpenAPI.yaml#/BranchDto'\n    DiffDto:\n      $ref: 'Dto.Components.OpenAPI.yaml#/DiffDto'\n    OrganizationDto:\n      $ref: 'Dto.Components.OpenAPI.yaml#/OrganizationDto'\n    OwnerDto:\n      $ref: 'Dto.Components.OpenAPI.yaml#/OwnerDto'\n    ReferenceDto:\n      $ref: 'Dto.Components.OpenAPI.yaml#/ReferenceDto'\n    RepositoryDto:\n      $ref: 'Dto.Components.OpenAPI.yaml#/RepositoryDto'\n\n    OrganizationParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n    CreateOrganizationParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/CreateOrganizationParameters'\n    SetOrganizationNameParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/SetOrganizationNameParameters'\n    SetOrganizationTypeParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/SetOrganizationTypeParameters'\n    SetOrganizationSearchVisibilityParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/SetOrganizationSearchVisibilityParameters'\n    SetOrganizationDescriptionParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/SetOrganizationDescriptionParameters'\n    DeleteOrganizationParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/DeleteOrganizationParameters'\n    UndeleteOrganizationParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/UndeleteOrganizationParameters'\n    GetOrganizationParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/GetOrganizationParameters'\n    ListRepositoriesParameters:\n      $ref: 'Organization.Components.OpenAPI.yaml#/ListRepositoriesParameters'\n\n\n    OwnerParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n    CreateOwnerParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/CreateOwnerParameters'\n    SetOwnerNameParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/SetOwnerNameParameters'\n    SetOwnerTypeParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/SetOwnerTypeParameters'\n    SetOwnerSearchVisibilityParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/SetOwnerSearchVisibilityParameters'\n    SetOwnerDescriptionParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/SetOwnerDescriptionParameters'\n    DeleteOwnerParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/DeleteOwnerParameters'\n    UndeleteOwnerParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/UndeleteOwnerParameters'\n    GetOwnerParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/GetOwnerParameters'\n    ListOrganizationsParameters:\n      $ref: 'Owner.Components.OpenAPI.yaml#/ListOrganizationsParameters'\n\n    ReferenceParameters:\n      $ref: 'Reference.Components.OpenAPI.yaml#/ReferenceParameters'\n    \n    RepositoryParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n    CreateRepositoryParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/CreateRepositoryParameters'\n    IsEmptyParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/IsEmptyParameters'\n    InitParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/InitParameters'\n    SetRepositoryVisibilityParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetRepositoryVisibilityParameters'\n    SetRepositoryStatusParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetRepositoryStatusParameters'\n    RecordSavesParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/RecordSavesParameters'\n    SetSaveDaysParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetSaveDaysParameters'\n    SetCheckpointDaysParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetCheckpointDaysParameters'\n    SetRepositoryDescriptionParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetRepositoryDescriptionParameters'\n    SetDefaultServerApiVersionParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetDefaultServerApiVersionParameters'\n    SetRepositoryNameParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/SetRepositoryNameParameters'\n    DeleteRepositoryParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/DeleteRepositoryParameters'\n    EnablePromotionTypeParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/EnablePromotionTypeParameters'\n    GetReferencesByReferenceIdParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/GetReferencesByReferenceIdParameters'\n    GetBranchesParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/GetBranchesParameters'\n    GetBranchesByBranchIdParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/GetBranchesByBranchIdParameters'\n    UndeleteRepositoryParameters:\n      $ref: 'Repository.Components.OpenAPI.yaml#/UndeleteRepositoryParameters'\n\n    Instant:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/Instant'\n    ProblemDetails:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ProblemDetails'\n    BranchId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/BranchId'\n    BranchName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/BranchName'\n    CorrelationId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CorrelationId'\n    ContainerName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ContainerName'\n    DirectoryId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryId'\n    FilePath:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/FilePath'\n    GraceIgnoreEntry:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceIgnoreEntry'\n    OrganizationId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationId'\n    OrganizationName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationName'\n    OwnerId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OwnerId'\n    OwnerName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OwnerName'\n    ReferenceId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId'\n    ReferenceName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceName'\n    ReferenceText:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceText'\n    RepositoryId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryId'\n    RepositoryName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryName'\n    RelativePath:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RelativePath'\n    Sha256Hash:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash'\n    StorageAccountName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/StorageAccountName'\n    StorageConnectionString:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/StorageConnectionString'\n    StorageContainerName:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/StorageContainerName'\n    UriWithSharedAccessSignature:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/UriWithSharedAccessSignature'\n    UserId:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/UserId'\n    OwnerType:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OwnerType'\n    OrganizationType:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/OrganizationType'\n    SearchVisibility:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/SearchVisibility'\n    ReferenceType:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceType'\n    CommonParameters:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CommonParameters'\n    FileVersion:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/FileVersion'\n    DirectoryVersion:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DirectoryVersion'\n    EventMetadata:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/EventMetadata'\n    GraceReturnValue:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceReturnValue'\n    GraceError:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceError'\n    GraceResult:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceResult'\n    FileSystemEntryType:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/FileSystemEntryType'\n    DifferenceType:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DifferenceType'\n    FileSystemDifference:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/FileSystemDifference'\n    FileDiff:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/FileDiff'\n    DiffPiece:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/DiffPiece'\n    ChangeType:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ChangeType'\n    ObjectStorageProvider:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ObjectStorageProvider'\n    RepositoryVisibility:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryVisibility'\n    RepositoryStatus:\n      $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/RepositoryStatus'\n\n  securitySchemes:\n    api_key:\n      type: apiKey\n      name: X-API-Key\n      in: header\n    bearerAuth: # arbitrary name for the security scheme\n      type: http\n      scheme: bearer\n      bearerFormat: JWT # optional, arbitrary value for documentation purposes\n"
  },
  {
    "path": "src/OpenAPI/Organization.Components.OpenAPI.yaml",
    "content": "    OrganizationParameters:\n      description: Parameters for many endpoints in the /organization path.\n      allOf:\n        - $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CommonParameters'\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          type: string\n        OrganizationId:\n          type: string\n        OrganizationName:\n          type: string\n    CreateOrganizationParameters:\n      description: Parameters for the /organization/create endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n    SetOrganizationNameParameters:\n      description: Parameters for the /organization/setName endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n      properties:\n        NewName:\n          type: string\n    SetOrganizationTypeParameters:\n      description: Parameters for the /organization/setType endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n      properties:\n        OrganizationType:\n          type: string\n    SetOrganizationSearchVisibilityParameters:\n      description: Parameters for the /organization/setSearchVisibility endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n      properties:\n        SearchVisibility:\n          type: string\n    SetOrganizationDescriptionParameters:\n      description: Parameters for the /organization/setDescription endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n      properties:\n        Description:\n          type: string\n    DeleteOrganizationParameters:\n      description: Parameters for the /organization/delete endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n      properties:\n        Force:\n          type: boolean\n        DeleteReason:\n          type: string\n    UndeleteOrganizationParameters:\n      description: Parameters for the /organization/undelete endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n    GetOrganizationParameters:\n      description: Parameters for the /organization/get endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n    ListRepositoriesParameters:\n      description: Parameters for the /organization/listRepositories endpoint.\n      allOf:\n        - $ref: 'Organization.Components.OpenAPI.yaml#/OrganizationParameters'\n"
  },
  {
    "path": "src/OpenAPI/Organization.Paths.OpenAPI.yaml",
    "content": "  /organization/create:\n    post:\n      summary: Create an organization.\n      description: |\n        ### Validation rules\n        - `OwnerId` must be a valid non-empty `Guid`.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` is required and must not be empty.\n        - `OrganizationName` must be a valid Grace name.\n        - The specified `OwnerId` or `OwnerName` must exist.\n        - The specified `OrganizationId` must not already exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/CreateOrganizationParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/setName:\n    post:\n      summary: Set the name of an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `NewName` is required and must not be empty.\n        - `NewName` must be a valid Grace name.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/SetOrganizationNameParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/setType:\n    post:\n      summary: Set the type of an organization (Public, Private).\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `OrganizationType` is required and must be a valid organization type.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/SetOrganizationTypeParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/setSearchVisibility:\n    post:\n      summary: Set the search visibility of an organization (Visible, Hidden).\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `SearchVisibility` is required and must be a valid search visibility option.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/SetOrganizationSearchVisibilityParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/setDescription:\n    post:\n      summary: Set the description of an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `Description` is required and must not be empty.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/SetOrganizationDescriptionParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/listRepositories:\n    post:\n      summary: List the repositories of an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/ListRepositoriesParameters\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: array\n                  $ref: Dto.Components.OpenAPI.yaml#/RepositoryDto\n\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/delete:\n    post:\n      summary: Delete an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - `DeleteReason` is required and must not be empty.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/DeleteOrganizationParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/undelete:\n    post:\n      summary: Undelete a previously deleted organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/OrganizationParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /organization/get:\n    post:\n      summary: Get an organization.\n      description: |\n        ### Validation rules\n        - `OrganizationId` is required and must not be empty.\n        - `OrganizationId` must be a valid non-empty `Guid`.\n        - `OrganizationName` must be a valid Grace name.\n        - The specified `OrganizationId` must exist.\n        - The specified organization must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Organization.Components.OpenAPI.yaml#/GetOrganizationParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n"
  },
  {
    "path": "src/OpenAPI/Owner.Components.OpenAPI.yaml",
    "content": "    OwnerParameters:\n      description: Parameters for many endpoints in the /owner path.\n      allOf:\n        - $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CommonParameters'\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          type: string\n    CreateOwnerParameters:\n      description: Parameters for the /owner/create endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n    SetOwnerNameParameters:\n      description: Parameters for the /owner/setName endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n      properties:\n        NewName:\n          type: string\n    SetOwnerTypeParameters:\n      description: Parameters for the /owner/setType endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n      properties:\n        OwnerType:\n          type: string\n    SetOwnerSearchVisibilityParameters:\n      description: Parameters for the /owner/setSearchVisibility endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n      properties:\n        SearchVisibility:\n          type: string\n    SetOwnerDescriptionParameters:\n      description: Parameters for the /owner/setDescription endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n      properties:\n        Description:\n          type: string\n    GetOwnerParameters:\n      description: Parameters for the /owner/get endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n    DeleteOwnerParameters:\n      description: Parameters for the /owner/delete endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n      properties:\n        Force:\n          type: boolean\n        DeleteReason:\n          type: string\n    UndeleteOwnerParameters:\n      description: Parameters for the /owner/undelete endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n    ListOrganizationsParameters:\n      description: Parameters for the /owner/listOrganizations endpoint.\n      allOf:\n        - $ref: 'Owner.Components.OpenAPI.yaml#/OwnerParameters'\n"
  },
  {
    "path": "src/OpenAPI/Owner.Paths.OpenAPI.yaml",
    "content": "  /owner/create:\n    post:\n      summary: Create an owner.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must not be empty.\n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must not be empty.\n        - `OwnerName` must be a valid Grace name.\n        - Owner with the same `OwnerId` must not already exist.\n        - Owner with the same `OwnerName` must not already exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: 'Owner.Components.OpenAPI.yaml#/CreateOwnerParameters'\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/setName:\n    post:\n      summary: Set the name of an owner.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `NewName` must not be empty.\n        - `NewName` must be a valid Grace name.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n        - Owner with the specified `NewName` must not already exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/SetOwnerNameParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/setType:\n    post:\n      summary: Set the owner type (Public, Private).\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `OwnerType` must not be empty.\n        - `OwnerType` must be a valid owner type.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/SetOwnerTypeParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/setSearchVisibility:\n    post:\n      summary: Set the owner search visibility (Visible, NotVisible).\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `SearchVisibility` must be a valid search visibility.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/SetOwnerSearchVisibilityParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/setDescription:\n    post:\n      summary: Set the owner's description.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `Description` must not be empty.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/SetOwnerDescriptionParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/listOrganizations:\n    post:\n      summary: List the organizations for an owner.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/ListOrganizationsParameters\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: Dto.Components.OpenAPI.yaml#/OrganizationDto\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/delete:\n    post:\n      summary: Delete an owner.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - `DeleteReason` must not be empty.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/DeleteOwnerParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/undelete:\n    post:\n      summary: Undelete a previously-deleted owner.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n        - `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - Owner with the specified `OwnerId` or `OwnerName` must exist and be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/OwnerParameters\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  /owner/get:\n    post:\n      summary: Get an owner.\n      description: |\n        ### Validation rules\n        \n        - `OwnerId` must be a valid GUID.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Owner.Components.OpenAPI.yaml#/GetOwnerParameters\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: Dto.Components.OpenAPI.yaml#/OwnerDto\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n"
  },
  {
    "path": "src/OpenAPI/Reference.Components.OpenAPI.yaml",
    "content": "    ReferenceParameters:\n      description: Parameters for many endpoints in the /reference path.\n      type: object\n      properties:\n        ReferenceId:\n          type: string\n          description: Unique identifier of the reference.\n        ReferenceType:\n          type: string\n          description: Type of the reference.\n        RepositoryText:\n          type: string\n          description: Textual representation of the repository.\n        RepositoryName:\n          type: string\n          description: Name of the repository.\n      allOf:\n        - $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/CommonParameters'\n"
  },
  {
    "path": "src/OpenAPI/Repository.Components.OpenAPI.yaml",
    "content": "    RepositoryParameters:\n      description: Parameters for many endpoints in the /repository path.\n      type: object\n      properties:\n        OwnerId:\n          type: string\n        OwnerName:\n          type: string\n        OrganizationId:\n          type: string\n        OrganizationName:\n          type: string\n        RepositoryId:\n          type: string\n        RepositoryName:\n          type: string\n    CreateRepositoryParameters:\n      description: Parameters for the /repository/create endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n    IsEmptyParameters:\n      description: Parameters for the /repository/isEmpty endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n    InitParameters:\n      description: Parameters for the /repository/init endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        GraceConfig:\n          type: string\n    SetRepositoryVisibilityParameters:\n      description: Parameters for the /repository/setVisibility endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        Visibility:\n          type: string\n    SetRepositoryStatusParameters:\n      description: Parameters for the /repository/setStatus endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        Status:\n          type: string\n    RecordSavesParameters:\n      description: Parameters for the /repository/recordSaves endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        RecordSaves:\n          type: boolean\n    SetSaveDaysParameters:\n      description: Parameters for the /repository/setSaveDays endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        SaveDays:\n          type: number\n    SetCheckpointDaysParameters:\n      description: Parameters for the /repository/setCheckpointDays\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        CheckpointDays:\n          type: number\n    SetRepositoryDescriptionParameters:\n      description: Parameters for the /repository/setDescription endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        Description:\n          type: string\n    SetDefaultServerApiVersionParameters:\n      description: Parameters for the /repository/setDefaultServerApiVersion endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        DefaultServerApiVersion:\n          type: string\n    SetRepositoryNameParameters:\n      description: Parameters for the /repository/setName endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        NewName:\n          type: string\n    DeleteRepositoryParameters:\n      description: Parameters for the /repository/delete endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        Force:\n          type: boolean\n        DeleteReason:\n          type: string\n    EnablePromotionTypeParameters:\n      description: Parameters for enabling promotion type.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        Enabled:\n          type: boolean\n    GetReferencesByReferenceIdParameters:\n      description: Parameters for the /repository/getReferencesByReferenceId endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        ReferenceIds:\n          type: array\n          items:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ReferenceId'\n        MaxCount:\n          type: integer\n    GetBranchesParameters:\n      description: Parameters for the /repository/getBranched endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        IncludeDeleted:\n          type: boolean\n        MaxCount:\n          type: integer\n    GetBranchesByBranchIdParameters:\n      description: Parameters for the /repository/getBranchesByBranchId endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n      properties:\n        BranchIds:\n          type: array\n          items:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/BranchId'\n        MaxCount:\n          type: integer\n        IncludeDeleted:\n          type: boolean\n    UndeleteRepositoryParameters:\n      description: Parameters for the /repository/undelete endpoint.\n      allOf:\n        - $ref: 'Repository.Components.OpenAPI.yaml#/RepositoryParameters'\n"
  },
  {
    "path": "src/OpenAPI/Repository.Paths.OpenAPI.yaml",
    "content": "  /repository/create:\n    post:\n      summary: Create a new repository.\n      description: |\n        This endpoint creates a new repository.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must not be empty.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The `RepositoryName` must not be empty.\n        - The `RepositoryName` must be a valid Grace name.\n        - The `OwnerId` must correspond to an existing owner.\n        - The `OrganizationId` must correspond to an existing organization.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/CreateRepositoryParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setVisibility:\n    post:\n      summary: Sets the search visibility of the repository.\n      description: |\n        This endpoint sets the search visibility of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `Visibility` value must be valid.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/SetRepositoryVisibilityParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setSaveDays:\n    post:\n      summary: Sets the number of days to keep saves in the repository.\n      description: |\n        This endpoint sets the number of days to keep saves in a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `SaveDays` value must be valid.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/SetSaveDaysParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setCheckpointDays:\n    post:\n      summary: Sets the number of days to keep checkpoints in the repository.\n      description: |\n        This endpoint sets the number of days to keep checkpoints in a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `CheckpointDays` value must be valid.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/SetCheckpointDaysParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setStatus:\n    post:\n      summary: Sets the status of the repository (Public, Private).\n      description: |\n        This endpoint sets the status of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `Status` value must be a valid repository status.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/SetRepositoryStatusParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setDefaultServerApiVersion:\n    post:\n      summary: Sets the default server API version for the repository.\n      description: |\n        This endpoint sets the default server API version for a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `DefaultServerApiVersion` must not be empty.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/SetDefaultServerApiVersionParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setRecordSaves:\n    post:\n      summary: Sets whether or not to keep saves in the repository.\n      description: |\n        This endpoint sets whether or not to keep saves in a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/RecordSavesParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/setDescription:\n    post:\n      summary: Sets the description of the repository.\n      description: |\n        This endpoint sets the description of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `Description` must not be empty.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/SetRepositoryDescriptionParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/delete:\n    post:\n      summary: Deletes the repository.\n      description: |\n        This endpoint deletes a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `DeleteReason` must not be empty.\n        - The repository must exist.\n        - The repository must not be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/DeleteRepositoryParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/undelete:\n    post:\n      summary: Undeletes a previously-deleted repository.\n      description: |\n        This endpoint undeletes a previously-deleted repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The repository must exist.\n        - The repository must be deleted.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/RepositoryParameters\n\n      responses:\n        '200':\n          $ref: './Responses.OpenAPI.yaml#/200'\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/exists:\n    post:\n      summary: Checks if a repository exists with the given parameters.\n      description: |\n        This endpoint checks if a repository exists based on the provided parameters.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/RepositoryParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: boolean\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n  \n  /repository/isEmpty:\n    post:\n      summary: Checks if a repository is empty.\n      description: |\n        This endpoint checks if a repository is empty, meaning it has just been created and has no data.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/RepositoryParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: boolean\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/get:\n    post:\n      summary: Gets a repository.\n      description: |\n        This endpoint retrieves the details of a repository.\n\n        ### Validation rules\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - The `RepositoryName` must not be empty.\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/RepositoryParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                $ref: Dto.Components.OpenAPI.yaml#/RepositoryDto\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/getBranches:\n    post:\n      summary: Gets a repository's branches.\n      description: |\n        This endpoint retrieves the branches of a repository.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `MaxCount` must be a number between 1 and 1000 (inclusive).\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/GetBranchesParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: Dto.Components.OpenAPI.yaml#/BranchDto\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/getReferencesByReferenceId:\n    post:\n      summary: Gets a list of references by reference IDs.\n      description: |\n        This endpoint retrieves a list of references based on the provided reference IDs.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `ReferenceIds` must not be empty.\n        - The `MaxCount` must be a number between 1 and 1000 (inclusive).\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/GetReferencesByReferenceIdParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: Dto.Components.OpenAPI.yaml#/ReferenceDto\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n\n  /repository/getBranchesByBranchId:\n    post:\n      summary: Gets a list of branches by branch IDs.\n      description: |\n        This endpoint retrieves a list of branches based on the provided branch IDs.\n\n        ### Validation rules\n        - The `OwnerId` must be a valid and non-empty GUID.\n        - The `OwnerName` must be a valid Grace name.\n        - Either `OwnerId` or `OwnerName` must be provided.\n        - The `OrganizationId` must be a valid and non-empty GUID.\n        - The `OrganizationName` must be a valid Grace name.\n        - Either `OrganizationId` or `OrganizationName` must be provided.\n        - The `RepositoryId` must be a valid and non-empty GUID.\n        - Either `RepositoryId` or `RepositoryName` must be provided.\n        - The `BranchIds` must not be empty.\n        - The `MaxCount` must be a number between 1 and 1000 (inclusive).\n        - The repository ID must not exist.\n\n        ### Errors and Problem Details\n        Error responses will be in the problem details JSON format, as defined in [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807).\n\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: Repository.Components.OpenAPI.yaml#/GetBranchesByBranchIdParameters\n\n      responses:\n        '200':\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                $ref: Dto.Components.OpenAPI.yaml#/BranchDto\n        '400':\n          $ref: './Responses.OpenAPI.yaml#/400'\n        '500':\n          $ref: './Responses.OpenAPI.yaml#/500'\n"
  },
  {
    "path": "src/OpenAPI/Responses.OpenAPI.yaml",
    "content": "    # This is the default response for all endpoints returning a 200 status code.\n    # Endpoints returning a specific type will define that in the OpenAPI specification for that path.\n    '200':\n      description: OK\n      content:\n        application/json:\n          schema:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceReturnValue'\n    '400':\n      description: Bad Request\n      content:\n        application/json:\n          schema:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ProblemDetails'\n    '500':\n      description: Internal Server Error\n      content:\n        application/json:\n          schema:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/ProblemDetails'\n    'OK':\n      description: OK\n      content:\n        application/json:\n          schema:\n            $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceReturnValue'\n"
  },
  {
    "path": "src/OpenAPI/Shared.Components.OpenAPI.yaml",
    "content": "components:\n  schemas:\n    Instant:\n      type: string\n      format: date-time\n    ProblemDetails:\n      type: object\n      properties:\n        type:\n          type: string\n          description: A URI reference that identifies the problem type.\n        title:\n          type: string\n          description: A short, human-readable summary of the problem type.\n        status:\n          type: integer\n          description: The HTTP status code generated by the origin server for this occurrence of the problem.\n        detail:\n          type: string\n          description: A human-readable explanation specific to this occurrence of the problem.\n        instance:\n          type: string\n          description: A URI reference that identifies the specific occurrence of the problem.\n      required:\n        - type\n        - title\n        - status\n        - detail\n        - instance\n    BranchId:\n      type: string\n      format: uuid\n      example: de7bf47d-23ae-4599-af68-68a317ea390d\n    BranchName:\n      type: string\n      example: MyBranch\n    CorrelationId:\n      type: string\n      description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n    ContainerName:\n      type: string\n      description: The name of the container must be a valid Grace name. (See the Grace documentation for more information.)\n    DirectoryId:\n      type: string\n      format: uuid\n      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n    FilePath:\n      type: string\n    GraceIgnoreEntry:\n      type: string\n    OrganizationId:\n      type: string\n      format: uuid\n      example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n    OrganizationName:\n      type: string\n    OwnerId:\n      type: string\n      format: uuid\n      example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n    OwnerName:\n      type: string\n    ReferenceId:\n      type: string\n      format: uuid\n      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n    ReferenceName:\n      type: string\n    ReferenceText:\n      type: string\n    RepositoryId:\n      type: string\n      format: uuid\n      example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n    RepositoryName:\n      type: string\n    RelativePath:\n      type: string\n    Sha256Hash:\n      type: string\n      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n    StorageAccountName:\n      type: string\n    StorageConnectionString:\n      type: string\n    StorageContainerName:\n      type: string\n    UriWithSharedAccessSignature:\n      type: string\n    UserId:\n      type: string\n\n    OwnerType:\n      type: string\n      enum: [Public, Private]\n      example: Public\n\n    OrganizationType:\n      type: string\n      enum: [Public, Private]\n      summary: The OrganizationType determines whether the Organization is visible to the public or not.\n      description: The OrganizationType determines whether the Organization is visible to the public or not.\n      example: Public\n\n    SearchVisibility:\n      type: string\n      enum: [Visible, NotVisible]\n      example: NotVisible\n\n    ReferenceType:\n      type: string\n      enum: [Promotion, Commit, Checkpoint, Save, Tag]\n      example: Commit\n    \n    CommonParameters:\n      description: Common parameters that are used across (almost) every endpoint in Grace.\n      type: object\n      properties:\n        CorrelationId:\n          type: string\n          description: A unique identifier for correlating logs and telemetry.\n        Principal:\n          type: string\n          description: The entity on whose behalf the action is being performed.\n\n    FileVersion:\n      type: object\n      properties:\n        Class:\n          type: string\n        RepositoryId:\n          - $ref: '#/components/schemas/RepositoryId'\n        RelativePath:\n          - $ref: '#/components/schemas/RelativePath'\n        Sha256Hash:\n          - $ref: '#/components/schemas/Sha256Hash'\n        IsBinary:\n          type: boolean\n        Size:\n          type: integer\n          format: int64\n        CreatedAt:\n          type: string\n          format: date-time\n        BlobUri:\n          type: string\n\n    DirectoryVersion:\n      type: object\n      properties:\n        Class:\n          type: string\n        DirectoryId:\n          - $ref: '#/components/schemas/DirectoryId'\n        RepositoryId:\n          - $ref: '#/components/schemas/RepositoryId'\n        RelativePath:\n          - $ref: '#/components/schemas/RelativePath'\n        Sha256Hash:\n          - $ref: '#/components/schemas/Sha256Hash'\n        Directories:\n          type: array\n          items:\n            - $ref: '#/components/schemas/DirectoryId'\n        Files:\n          type: array\n          items:\n            - $ref: '#/components/schemas/FileVersion'\n        Size:\n          type: integer\n          format: int64\n        RecursiveSize:\n          type: integer\n          format: int64\n        CreatedAt:\n          type: string\n          format: date-time\n\n    EventMetadata:\n      description: \n      type: object\n      properties:\n        Timestamp:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Principal:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n\n    GraceReturnValue:\n      type: object\n      properties:\n        ReturnValue:\n          type: object\n        EventTime:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n\n    GraceError:\n      type: object\n      properties:\n        Error:\n          type: string\n        EventTime:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n\n    GraceResult:\n      oneOf:\n        - - $ref: '#/components/schemas/GraceReturnValue'\n        - - $ref: '#/components/schemas/GraceError'\n\n    FileSystemEntryType:\n      type: string\n      enum:\n        - Directory\n        - File\n\n    DifferenceType:\n      type: string\n      enum:\n        - Add\n        - Change\n        - Delete\n\n    FileSystemDifference:\n      type: object\n      properties:\n        DifferenceType:\n          $ref: #/DifferenceType\n        FileSystemEntryType:\n          $ref: #/FileSystemEntryType\n        RelativePath:\n          $ref: #/RelativePath\n      required:\n        - DifferenceType\n        - FileSystemEntryType\n        - RelativePath\n\n    FileDiff:\n      type: object\n      properties:\n        RelativePath:\n          $ref: #/RelativePath\n        FileSha1:\n          $ref: #/Sha256Hash\n        CreatedAt1:\n          $ref: #/Instant\n        FileSha2:\n          $ref: #/Sha256Hash\n        CreatedAt2:\n          $ref: #/Instant\n        IsBinary:\n          type: boolean\n        InlineDiff:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: #/DiffPiece\n        SideBySideOld:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: #/DiffPiece\n        SideBySideNew:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: #/DiffPiece\n      required:\n        - RelativePath\n        - FileSha1\n        - CreatedAt1\n        - FileSha2\n        - CreatedAt2\n        - IsBinary\n        - InlineDiff\n        - SideBySideOld\n        - SideBySideNew\n\n    DiffPiece:\n      type: object\n      properties:\n        Position:\n          type: integer\n          nullable: true\n        SubPieces:\n          type: array\n          items:\n            $ref: #/DiffPiece\n        Text:\n          type: string\n        Type:\n          $ref: #/ChangeType\n      required:\n        - Text\n        - Type\n\n    ChangeType:\n      type: integer\n      enum:\n        - 0\n        - 1\n        - 2\n        - 3\n        - 4\n      enumNames:\n        - Unchanged\n        - Deleted\n        - Inserted\n        - Imaginary\n        - Modified\n\n    ObjectStorageProvider:\n      type: string\n      enum:\n        - AWSS3\n        - AzureBlobStorage\n        - GoogleCloudStorage\n        - Unknown\n\n    RepositoryVisibility:\n      type: string\n      enum:\n        - Private\n        - Public\n\n    RepositoryStatus:\n      type: string\n      enum:\n        - Active\n        - Suspended\n        - Closed\n        - Deleted\n"
  },
  {
    "path": "src/OpenAPI/Shared2.Components.OpenAPI.yaml",
    "content": "    Instant:\n      type: string\n      format: date-time\n    ProblemDetails:\n      type: object\n      properties:\n        type:\n          type: string\n          description: A URI reference that identifies the problem type.\n        title:\n          type: string\n          description: A short, human-readable summary of the problem type.\n        status:\n          type: integer\n          description: The HTTP status code generated by the origin server for this occurrence of the problem.\n        detail:\n          type: string\n          description: A human-readable explanation specific to this occurrence of the problem.\n        instance:\n          type: string\n          description: A URI reference that identifies the specific occurrence of the problem.\n      required:\n        - type\n        - title\n        - status\n        - detail\n        - instance\n    BranchId:\n      type: string\n      format: uuid\n      example: de7bf47d-23ae-4599-af68-68a317ea390d\n    BranchName:\n      type: string\n      example: MyBranch\n    CorrelationId:\n      type: string\n      description: If you don't provide a CorrelationId, a Guid will be generated for you. (You're welcome.) CorrelationId's are just strings; feel free to send any kind of CorrelationId you prefer.\n    ContainerName:\n      type: string\n      description: The name of the container must be a valid Grace name. (See the Grace documentation for more information.)\n    DirectoryId:\n      type: string\n      format: uuid\n      example: 33a4e36b-828f-4fae-9343-50b6560dc842\n    FilePath:\n      type: string\n    GraceIgnoreEntry:\n      type: string\n    OrganizationId:\n      type: string\n      format: uuid\n      example: e35d64a9-b990-44f5-bf02-32ad7d15630c\n    OrganizationName:\n      type: string\n    OwnerId:\n      type: string\n      format: uuid\n      example: 9dd5f81f-dc43-4839-9173-85d09394f30f\n    OwnerName:\n      type: string\n    ReferenceId:\n      type: string\n      format: uuid\n      example: c8f9bac8-d489-46c7-917f-b36b7d9efa9a\n    ReferenceName:\n      type: string\n    ReferenceText:\n      type: string\n    RepositoryId:\n      type: string\n      format: uuid\n      example: ab6f35ef-6e01-440b-8f9b-c343a5272095\n    RepositoryName:\n      type: string\n    RelativePath:\n      type: string\n    Sha256Hash:\n      type: string\n      example: 805331A98813206270E35564769E8BB59EEA02AEB7B27C7D6C63E625E1857243\n    StorageAccountName:\n      type: string\n    StorageConnectionString:\n      type: string\n    StorageContainerName:\n      type: string\n    UriWithSharedAccessSignature:\n      type: string\n    UserId:\n      type: string\n\n    OwnerType:\n      type: string\n      enum: [Public, Private]\n      example: Public\n\n    OrganizationType:\n      type: string\n      enum: [Public, Private]\n      summary: The OrganizationType determines whether the Organization is visible to the public or not.\n      description: The OrganizationType determines whether the Organization is visible to the public or not.\n      example: Public\n\n    SearchVisibility:\n      type: string\n      enum: [Visible, NotVisible]\n      example: NotVisible\n\n    ReferenceType:\n      type: string\n      enum: [Promotion, Commit, Checkpoint, Save, Tag]\n      example: Commit\n    \n    CommonParameters:\n      description: Common parameters that are used across (almost) every endpoint in Grace.\n      type: object\n      properties:\n        CorrelationId:\n          type: string\n          description: A unique identifier for correlating logs and telemetry.\n        Principal:\n          type: string\n          description: The entity on whose behalf the action is being performed.\n\n    FileVersion:\n      type: object\n      properties:\n        Class:\n          type: string\n        RepositoryId:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/RepositoryId'\n        RelativePath:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/RelativePath'\n        Sha256Hash:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/Sha256Hash'\n        IsBinary:\n          type: boolean\n        Size:\n          type: integer\n          format: int64\n        CreatedAt:\n          type: string\n          format: date-time\n        BlobUri:\n          type: string\n\n    DirectoryVersion:\n      type: object\n      properties:\n        Class:\n          type: string\n        DirectoryId:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/DirectoryId'\n        RepositoryId:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/RepositoryId'\n        RelativePath:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/RelativePath'\n        Sha256Hash:\n          - $ref: 'Shared.Components.OpenAPI.yaml#/Sha256Hash'\n        Directories:\n          type: array\n          items:\n            - $ref: 'Shared.Components.OpenAPI.yaml#/DirectoryId'\n        Files:\n          type: array\n          items:\n            - $ref: 'Shared.Components.OpenAPI.yaml#/FileVersion'\n        Size:\n          type: integer\n          format: int64\n        RecursiveSize:\n          type: integer\n          format: int64\n        CreatedAt:\n          type: string\n          format: date-time\n\n    EventMetadata:\n      description: \n      type: object\n      properties:\n        Timestamp:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Principal:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n\n    GraceReturnValue:\n      type: object\n      properties:\n        ReturnValue:\n          type: object\n        EventTime:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n\n    GraceError:\n      type: object\n      properties:\n        Error:\n          type: string\n        EventTime:\n          type: string\n          format: date-time\n        CorrelationId:\n          type: string\n        Properties:\n          type: object\n          additionalProperties:\n            type: string\n\n    GraceResult:\n      oneOf:\n        - - $ref: 'Shared.Components.OpenAPI.yaml#/components/schemas/GraceReturnValue'\n        - - $ref: 'Shared.Components.OpenAPI.yaml#/GraceError'\n\n    FileSystemEntryType:\n      type: string\n      enum:\n        - Directory\n        - File\n\n    DifferenceType:\n      type: string\n      enum:\n        - Add\n        - Change\n        - Delete\n\n    FileSystemDifference:\n      type: object\n      properties:\n        DifferenceType:\n          $ref: Shared.Components.OpenAPI.yaml#/DifferenceType\n        FileSystemEntryType:\n          $ref: Shared.Components.OpenAPI.yaml#/FileSystemEntryType\n        RelativePath:\n          $ref: Shared.Components.OpenAPI.yaml#/RelativePath\n      required:\n        - DifferenceType\n        - FileSystemEntryType\n        - RelativePath\n\n    FileDiff:\n      type: object\n      properties:\n        RelativePath:\n          $ref: Shared.Components.OpenAPI.yaml#/RelativePath\n        FileSha1:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash\n        CreatedAt1:\n          $ref: Shared.Components.OpenAPI.yaml#/Instant\n        FileSha2:\n          $ref: Shared.Components.OpenAPI.yaml#/components/schemas/Sha256Hash\n        CreatedAt2:\n          $ref: Shared.Components.OpenAPI.yaml#/Instant\n        IsBinary:\n          type: boolean\n        InlineDiff:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: Shared.Components.OpenAPI.yaml#/DiffPiece\n        SideBySideOld:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: Shared.Components.OpenAPI.yaml#/DiffPiece\n        SideBySideNew:\n          type: array\n          items:\n            type: array\n            items:\n              $ref: Shared.Components.OpenAPI.yaml#/DiffPiece\n      required:\n        - RelativePath\n        - FileSha1\n        - CreatedAt1\n        - FileSha2\n        - CreatedAt2\n        - IsBinary\n        - InlineDiff\n        - SideBySideOld\n        - SideBySideNew\n\n    DiffPiece:\n      type: object\n      properties:\n        Position:\n          type: integer\n          nullable: true\n        SubPieces:\n          type: array\n          items:\n            $ref: Shared.Components.OpenAPI.yaml#/DiffPiece\n        Text:\n          type: string\n        Type:\n          $ref: Shared.Components.OpenAPI.yaml#/ChangeType\n      required:\n        - Text\n        - Type\n\n    ChangeType:\n      type: integer\n      enum:\n        - 0\n        - 1\n        - 2\n        - 3\n        - 4\n      enumNames:\n        - Unchanged\n        - Deleted\n        - Inserted\n        - Imaginary\n        - Modified\n\n    ObjectStorageProvider:\n      type: string\n      enum:\n        - AWSS3\n        - AzureBlobStorage\n        - GoogleCloudStorage\n        - Unknown\n\n    RepositoryVisibility:\n      type: string\n      enum:\n        - Private\n        - Public\n\n    RepositoryStatus:\n      type: string\n      enum:\n        - Active\n        - Suspended\n        - Closed\n        - Deleted\n"
  },
  {
    "path": "src/Processing file system changes.md",
    "content": "# Processing file system changes\n\n## Processing file and directory changes\n\n```mermaid\nflowchart LR\n    A[File system change detected] --> B{Directory or file?}\n    B -->|File| F{Add, change, or delete?}\n    B -->|Directory| D{Add, change, or delete?}\n    F -->|Add| FAdd(Create new LocalDirectoryVersion<br> by copying from existing <br>LocalDirectoryVersion and <br>adding file, then <br>recompute up the tree)\n    FAdd --> FAdd1(Identify parent)\n    FAdd1 --> FAdd2\n    FChange --> FChange1(Identify parent)\n    FDelete --> FDelete1(Identify parent)\n    F -->|Change| FChange(Replace file in existing LocalDirectoryVersion<br> and create new LocalDirectoryVersion)\n    F --> |Delete| FDelete(Delete directory from parent <br>LocalDirectoryVersion<br> and recompute parent)\n    R(Recalculate tree up to root)\n    D -->|Add| DAdd(Add new LocalDirectoryVersion)\n    D -->|Change| DChange(Create new LocalDirectoryVersion<br> from existing one)\n    D --> |Delete| DDelete(Delete directory from parent <br>LocalDirectoryVersion<br> and recompute parent)\n    DAdd --> DAdd1(Identify parent)\n    DAdd1 --> DAdd2(Create new LDV for new directory)\n    DAdd2 --> DAdd3(Create new LDV for parent)\n    DAdd3 --> R\n    DChange --> DChange1(Identify parent)\n    DChange1 --> DChange2(Create new LDV from existing<br> with replaced file information)\n    DChange2 --> DChange3(Create new LDV for parent)\n    DChange3 --> R\n    DDelete --> DDelete1(Identify parent)\n    DDelete1 --> DDelete2(Remove subdirectory from parent)\n    DDelete2 --> DDelete3(Create new LDV for parent)\n    DDelete3 --> R\n```\n"
  },
  {
    "path": "src/docs/ASPIRE_SETUP.md",
    "content": "# Grace.Server Local Development with .NET Aspire\n\n## Prerequisites\n\n1. **.NET 10 SDK** – Install the SDK pinned in `global.json` (or compatible\n   roll-forward).\n2. **Docker Desktop** – Enable Docker before starting Aspire:\n   - Windows/macOS: <https://www.docker.com/products/docker-desktop>\n   - Linux: install `docker.io` (Ubuntu) or the equivalent package for your\n     distro.\n3. **Resources** – Reserve at least 8 GB RAM and 10 GB free disk space. The\n   Cosmos DB emulator and SQL Server container for Service Bus both consume\n   several GB of memory.\n4. **Cosmos certificates** – With non-SSL ports enabled the emulator uses HTTP.\n   If you prefer HTTPS, export and trust the certificate as described in\n   [Azure Cosmos DB emulator for Linux](https://learn.microsoft.com/azure/cosmos-db/emulator-linux).\n\n## Preferred Local Loop\n\nUse the agent-ready scripts as the canonical entrypoint:\n\n```powershell\npwsh ./scripts/bootstrap.ps1\npwsh ./scripts/validate.ps1 -Fast\n```\n\nRun `pwsh ./scripts/validate.ps1 -Full` when you need the Aspire integration\ncoverage from `Grace.Server.Tests`.\n\n## Environment Variables\n\nFor the full environment variable inventory, `dotnet user-secrets` guidance,\nand profile-specific requirements (`DebugLocal` vs `DebugAzure`), use\n`src/docs/ENVIRONMENT.md` as the canonical reference.\n\n## Start Aspire\n\n> The Aspire app host lives in `Grace.Aspire.AppHost`. It orchestrates Azurite,\n> Cosmos DB, Redis, SQL Server, the Service Bus emulator, and `Grace.Server`\n> itself.\n\n### Visual Studio\n\n1. Set `Grace.Aspire.AppHost` as the startup project.\n2. Press **F5**. Visual Studio launches the Aspire host and opens the dashboard\n   at `http://localhost:18888`.\n\n### .NET CLI\n\n```bash\ncd Grace.Aspire.AppHost\nDOTNET_ENVIRONMENT=Development dotnet run\n```\n\nThe CLI host prints connection details for each emulator (Azurite, Redis,\nCosmos, Service Bus) as they start.\n\n## Verify Components\n\nOpen `http://localhost:18888` and confirm the following resources show\n`Running`:\n\n- `azurite` – Azure Storage emulator (blob/queue/table) on ports `10000-10002`\n- `redis` – Redis cache on port `6379`\n- `cosmos-emulator` – Cosmos DB emulator on port `8081`\n- `servicebus-sql` – SQL Server container required by the Service Bus emulator\n- `service-bus-emulator` – Service Bus emulator (AMQP on `5672`, management UI\n  on `9200`)\n- `grace-server` – HTTP `5000` / HTTPS `5001`\n\n## Smoke Tests\n\n1. **Health check**\n\n   ```bash\n   curl http://localhost:5000/healthz\n   ```\n\n   Expected output: `Grace server seems healthy!`\n2. **Traces & metrics** – In the Aspire dashboard, select **grace-server** →\n   **Traces** or **Metrics** to review OpenTelemetry data sent via the OTLP\n   exporter.\n3. **Logs** – Within the same resource view, confirm log entries for Orleans\n   startup and Aspire instrumentation.\n\n## Service Bus Emulator Connection String\n\nThe .NET SDK expects a connection string that ends with\n`UseDevelopmentEmulator=true`. After the emulator finishes booting:\n\n1. Browse to `http://localhost:9200` (Service Bus emulator management portal).\n2. Copy the `RootManageSharedAccessKey` connection string shown in the portal.\n3. Before launching Aspire, set the lowercase environment variable so\n   `Grace.Server` can connect:\n\n```powershell\n$env:azureservicebusconnectionstring =\n  \"Endpoint=sb://localhost/;\" +\n  \"SharedAccessKeyName=RootManageSharedAccessKey;\" +\n  \"SharedAccessKey=<SAS_KEY>;\" +\n  \"UseDevelopmentEmulator=true;\"\n\ndotnet run\n```\n\nEnvironment keys in `Grace.Shared.Constants` are lowercase. Microsoft Learn\nconfirms configuration keys are case-insensitive, so this works for Windows and\nLinux hosts.\n\n## Run Tests\n\nAfter the host is up:\n\n```bash\ncd ..\\Grace.Server.Tests\nDOTNET_ENVIRONMENT=Development dotnet test --no-build\n```\n\nIntegration tests reuse the running emulators. Shut down Aspire when tests\nfinish to release containers and ports.\n\n## Troubleshooting\n\n- **Port conflicts** – Update bindings inside\n  `Grace.Aspire.AppHost/Program.cs` if ports `5000`, `5001`, `10000–10002`,\n  `8081`, `10251–10255`, `5672`, `9200`, or `21433` are already used.\n- **Cosmos DB emulator** – First launch can take several minutes. Inspect logs\n  with `docker logs cosmos-emulator`.\n- **Service Bus emulator** – The Service Bus container waits for SQL Server. If\n  startup fails, check `docker logs servicebus-sql` for password or EULA issues.\n- **Missing telemetry** – `OTLP_ENDPOINT_URL` must reach\n  `http://localhost:18889`. In Azure, provide\n  `APPLICATIONINSIGHTS_CONNECTION_STRING` so Azure Monitor exporters activate.\n\n## Next Steps\n\n- Review `infra/app-service.bicep` for a production-ready App Service\n  deployment template.\n- Set GitHub secrets (`AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`,\n  `AZURE_CLIENT_ID`) before enabling the workflow in\n  `.github/workflows/deploy-to-app-service.yml`.\n"
  },
  {
    "path": "src/docs/ENVIRONMENT.md",
    "content": "# Grace Environment Inventory\n\nThis document summarizes environment dependencies, onboarding variables, and runtime settings used by Grace.\nPrimary sources are `src/Grace.Shared/Constants.Shared.fs` (`EnvironmentVariables`) and Aspire host\nconfiguration in `src/Grace.Aspire.AppHost/Program.Aspire.AppHost.cs`.\n\n## Docker Dependencies (Local Aspire)\n\nLocal Aspire runs containers and emulators for:\n\n- Azurite (Azure Storage emulator: blob, queue, table)\n- Azure Cosmos DB emulator\n- Azure Service Bus emulator (with a SQL Server container)\n- Redis\n\nThe Aspire dashboard defaults to <http://localhost:18888> and OTLP export defaults to\n`http://localhost:18889` unless overridden.\n\n## Aspire Run Modes\n\n- `ASPIRE_RESOURCE_MODE` (not in `EnvironmentVariables`):\n  - `Local` (default): uses emulators and containers.\n  - `Azure`: uses real Azure resources from config, user secrets, or environment.\n\n## Test Toggles\n\n- `GRACE_TESTING`: enables test mode in Aspire and local TestAuth paths.\n- `GRACE_TEST_CLEANUP`: set to `1` or `true` to enable cleanup in server tests.\n\n## Auth Forwarding In Aspire\n\n`Grace.Aspire.AppHost` forwards these auth settings into `Grace.Server` when present:\n\n- `grace__auth__oidc__authority`\n- `grace__auth__oidc__audience`\n- `grace__auth__oidc__cli_client_id` (publish mode)\n\n## DebugLocal Onboarding Variables\n\nFor local onboarding, `scripts/start-debuglocal.ps1` is canonical.\n`scripts/dev-local.ps1` remains a compatibility alias.\n\nBootstrap user resolution order is deterministic:\n\n1. `-BootstrapUserId` script parameter.\n2. First user in shell env `grace__authz__bootstrap__system_admin_users`.\n3. First user in launch profile value\n   `grace__authz__bootstrap__system_admin_users` from\n   `src/Grace.Aspire.AppHost/Properties/launchSettings.json`.\n4. `-BootstrapUserIdFallback` (defaults to `test-admin`).\n\nThe script prints both the resolved bootstrap user and the source used.\n\n`GRACE_TESTING` behavior during onboarding:\n\n- if `GRACE_TESTING` is already set in your shell, the script keeps that value;\n- if unset, the script sets `GRACE_TESTING=1` for local TestAuth startup.\n\nPowerShell:\n\n```powershell\npwsh ./scripts/start-debuglocal.ps1 -GraceServerUri \"http://localhost:5000\"\n```\n\nbash / zsh:\n\n```bash\npwsh ./scripts/start-debuglocal.ps1 --GraceServerUri \"http://localhost:5000\"\n```\n\n## DebugLocal Reliability Switches\n\nThese are script parameters (not environment variables):\n\n- `-SkipAuthProbe`: skip auth preflight (`/auth/oidc/config`, `/auth/me`).\n- `-NoTokenBootstrap`: skip PAT creation.\n- `-TokenBootstrapMaxAttempts`: set max token bootstrap attempts.\n- `-TokenBootstrapInitialBackoffSeconds`: set initial retry backoff.\n- `-StartupTimeoutSeconds`: set health wait timeout.\n- `-CleanupWaitSeconds`: set process cleanup wait timeout.\n\nPowerShell:\n\n```powershell\npwsh ./scripts/start-debuglocal.ps1 `\n  -TokenBootstrapMaxAttempts 5 `\n  -TokenBootstrapInitialBackoffSeconds 2\n```\n\nbash / zsh:\n\n```bash\npwsh ./scripts/start-debuglocal.ps1 \\\n  --TokenBootstrapMaxAttempts 5 \\\n  --TokenBootstrapInitialBackoffSeconds 2\n```\n\n## DebugLocal Diagnostics Artifacts\n\nOn failure, onboarding writes artifacts under `.grace/logs`:\n\n- `start-debuglocal-<run-id>.stdout.log`\n- `start-debuglocal-<run-id>.stderr.log`\n- `start-debuglocal-<run-id>.failure.json`\n- `start-debuglocal-<run-id>.runtime-metadata.json`\n\nThese capture failure classification, retryability, cleanup notes, and runtime metadata for troubleshooting.\n\n## Environment Variables (Canonical List)\n\n### Telemetry\n\n- `grace__applicationinsightsconnectionstring`: Application Insights connection string (optional).\n\n### Storage (Azure)\n\n- `grace__azure_storage__connectionstring`: Azure Storage connection string.\n- `grace__azure_storage__account_name`: Storage account name override (managed identity path).\n- `grace__azure_storage__endpoint_suffix`: Storage endpoint suffix override.\n- `grace__azure_storage__key`: Storage account key.\n- `grace__azure_storage__directoryversion_container_name`: Directory version container name.\n- `grace__azure_storage__diff_container_name`: Diff container name.\n- `grace__azure_storage__zipfile_container_name`: Zip container name.\n\n### Cosmos DB (Azure)\n\n- `grace__azurecosmosdb__connectionstring`: Cosmos DB connection string.\n- `grace__azurecosmosdb__endpoint`: Cosmos endpoint (managed identity path).\n- `grace__azurecosmosdb__database_name`: Cosmos database name.\n- `grace__azurecosmosdb__container_name`: Cosmos container name.\n\n### Service Bus (Azure)\n\n- `grace__azure_service_bus__connectionstring`: Service Bus connection string.\n- `grace__azure_service_bus__namespace`: Service Bus namespace.\n- `grace__azure_service_bus__topic`: Service Bus topic name.\n- `grace__azure_service_bus__subscription`: Service Bus subscription name.\n\n### Redis\n\n- `grace__redis__host`: Redis host.\n- `grace__redis__port`: Redis port.\n\n### Orleans\n\n- `orleans_cluster_id`: Orleans cluster ID.\n- `orleans_service_id`: Orleans service ID.\n\n### Pub/Sub Routing\n\n- `grace__pubsub__system`: Pub/sub provider selector (for example, `AzureServiceBus`).\n\n### Auth (OIDC / Auth0)\n\n- `grace__auth__oidc__authority`\n- `grace__auth__oidc__audience`\n- `grace__auth__oidc__cli_client_id`\n- `grace__auth__oidc__cli_redirect_port`\n- `grace__auth__oidc__cli_scopes`\n- `grace__auth__oidc__m2m_client_id`\n- `grace__auth__oidc__m2m_client_secret`\n- `grace__auth__oidc__m2m_scopes`\n\n### Auth (Microsoft, Deprecated)\n\n- `grace__auth__microsoft__client_id`\n- `grace__auth__microsoft__client_secret`\n- `grace__auth__microsoft__tenant_id`\n- `grace__auth__microsoft__authority`\n- `grace__auth__microsoft__api_scope`\n- `grace__auth__microsoft__cli_client_id`\n\n### Auth (PAT Defaults)\n\n- `grace__auth__pat__default_lifetime_days`\n- `grace__auth__pat__max_lifetime_days`\n- `grace__auth__pat__allow_no_expiry`\n\n### Authorization (Bootstrap)\n\n- `grace__authz__bootstrap__system_admin_users`: semicolon-delimited user IDs to seed SystemAdmin at system\n  scope when no assignments exist.\n- `grace__authz__bootstrap__system_admin_groups`: semicolon-delimited group IDs to seed SystemAdmin at system\n  scope when no assignments exist.\n\n### Metrics\n\n- `grace__metrics__allow_anonymous`: when `true`, allows anonymous access to `/metrics` (default `false`).\n\n### CLI / Client\n\n- `GRACE_SERVER_URI`: Grace server base URL (include port, omit trailing slash).\n- `GRACE_TOKEN`: PAT for non-interactive auth.\n- `GRACE_TOKEN_FILE`: override for token file path.\n\n### Reminders\n\n- `grace__reminder__batch__size`: batch size for reminder processing.\n\n### Diagnostics\n\n- `grace__debug_environment`: debug environment marker (for example, `Local`, `Azure`).\n- `grace__log_directory`: Grace server log directory.\n\n### Future / Placeholders\n\n- `grace__aws_sqs__queue_url`\n- `grace__aws_sqs__region`\n- `grace__gcp__projectid`\n- `grace__gcp__topic`\n- `grace__gcp__subscription`\n"
  },
  {
    "path": "src/fantomas-config.json",
    "content": "{\n  \"version\": 3,\n  \"indentSize\": 4,\n  \"maxLineLength\": 160,\n  \"endOfLine\": \"lf\",\n  \"insertFinalNewline\": true,\n  \"trimTrailingWhitespace\": true,\n  \"maxFunctionBindingWidth\": 160,\n  \"maxValueBindingWidth\": 160,\n  \"maxRecordWidth\": 160,\n  \"maxIfThenElseShortWidth\": 80,\n  \"maxIfThenShortWidth\": 80,\n  \"multilineBlockBracketsOnSameColumn\": true,\n  \"alignFunctionSignatureToIndentation\": true\n}\n"
  },
  {
    "path": "src/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Docker Compose\": {\n      \"commandName\": \"DockerCompose\",\n      \"commandVersion\": \"1.0\",\n      \"composeLaunchAction\": \"None\",\n      \"composeLaunchServiceName\": \"grace-server\",\n      \"serviceActions\": {\n        \"daprd\": \"StartWithoutDebugging\",\n        \"dapr-placement\": \"StartWithoutDebugging\",\n        \"grace-server\": \"StartDebugging\",\n        \"otel-collector\": \"StartWithoutDebugging\",\n        \"prometheus\": \"StartWithoutDebugging\",\n        \"redis\": \"StartWithoutDebugging\",\n        \"zipkin\": \"StartWithoutDebugging\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/nuget.config",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <add key=\"dotnet10\" value=\"https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json\" />\n    <add key=\"nuget.org\" value=\"https://api.nuget.org/v3/index.json\" protocolVersion=\"3\" />\n  </packageSources>\n  <packageSourceMapping>\n    <packageSource key=\"nuget.org\">\n      <package pattern=\"*\" />\n    </packageSource>\n    <packageSource key=\"dotnet10\">\n      <package pattern=\"*\" />\n    </packageSource>\n  </packageSourceMapping>\n</configuration>"
  }
]