Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ TypeScript frontend in `src/Scripts` and is built into `src/wwwroot`.

## Build and test

Run these commands from the repository root on Windows. A full build also requires Node.js and npm.

| Task | Command |
| --- | --- |
| Build and test | `build.cmd` |
Expand All @@ -30,6 +32,8 @@ or a CI checkout must be complete: with a shallow clone the build fails or produ

| Document | Read it before |
| --- | --- |
| [docs/design/architecture.md](docs/design/architecture.md) | changing startup, service ownership, UI threading, frontend/host contracts, API authentication, single-instance activation, or policy composition. |
| [docs/design/code-conventions.md](docs/design/code-conventions.md) | creating or editing any `.cs` file. Encoding and design rules apply from its first line, and a wrong encoding rewrites the whole file. |
| [docs/design/versioning.md](docs/design/versioning.md) | touching `version.json`, the build number, or any code that compares versions. |
| [docs/design/diagnostics.md](docs/design/diagnostics.md) | catching, logging, or reporting an exception, or touching the crash dialog, the diagnostics pane, or telemetry. |
| [docs/documentation-guidelines.md](docs/documentation-guidelines.md) | writing or updating anything under `docs/`. |
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
@AGENTS.md
@AGENTS.md
59 changes: 59 additions & 0 deletions docs/design/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Architecture and runtime boundaries

Bravo is a Windows desktop application. Its .NET process owns a WinForms window and an ASP.NET Core loopback API. The window embeds the TypeScript UI in WebView2; UI requests reach application services through the API. This document covers the boundaries that constrain changes, not individual feature implementations.

## Composition and resource ownership

[Program](../../src/Program.cs) configures the process, creates bootstrap services, installs global exception handling, then builds and runs the host. [BravoApplication](../../src/Host/BravoApplication.cs) resolves the single-instance role before starting Kestrel: a secondary process redirects activation and exits without starting its own server or window.

| Resource | Lifetime owner | Constraint for changes |
| --- | --- | --- |
| Bootstrap telemetry and logger factory | [BootstrapContext](../../src/Host/BootstrapContext.cs) | Keep them available until host disposal completes; telemetry is disposed before the logger factory |
| Bootstrap policy service and user settings | Bootstrap creates or obtains them; the container receives those instances | Preserve the shared instances so consumers do not silently use different configuration snapshots |
| Application instance and its pipe server | The host container owns `IBravoApplicationInstance`; that instance owns the pipe server | Releasing the pipe ends ownership of the application instance |
| Main window | `BravoApplication.Run` | The window is created through DI but is not registered as a container-owned service |

[BravoServiceCollectionExtensions](../../src/Host/BravoServiceCollectionExtensions.cs) registers bootstrap services as existing instances. Replacing those registrations with container-owned factories changes disposal responsibility and must preserve the ownership above. Global exception handling uses bootstrap services independently of host construction; its exact coverage is in [diagnostics](diagnostics.md#startup-coverage-and-fatal-reporting).

## UI and server threads

[AppWindow](../../src/Infrastructure/AppWindow.cs) is created on the STA entry thread, which runs the WinForms message loop. API work and single-instance activation callbacks do not have that UI-thread guarantee.

- Marshal operations on the window or WebView2 to the UI thread. Activation handlers use `ProcessHelper.InvokeOnUIThread` with the window as the control.
- Setting `SynchronizationContext.Current` is not thread dispatch. [ProcessHelper.RunWithUISynchronizationContext](../../src/Infrastructure/Helpers/ProcessHelper.cs) changes the ambient context only during a synchronous callback; do not treat it as permission to access controls from a server thread.
- Keep window activation and document delivery distinct: the window can exist before WebView2 is ready to receive a startup message.

Violating these boundaries can cause cross-thread UI failures or lost activation requests. The [single-instance contract](#single-instance-contract) describes the current startup limitation.

## Frontend and API contract

[AppWindow](../../src/Infrastructure/AppWindow.cs) injects `CONFIG` with the API address, token, options, policies and environment information. The frontend uses [Host](../../src/Scripts/controllers/host.ts) for API calls and WebView messages for desktop integration. The C# serializer and TypeScript consumers must agree on field names, nullability and enum values; changing only one side can compile in C# while breaking the UI. Follow the full-build requirement in [AGENTS.md](../../AGENTS.md#build-and-test) for changes to this contract.

[BravoApplicationBuilder](../../src/Host/BravoApplicationBuilder.cs) configures Kestrel and endpoint authorization:

| Build | Listening address | Controller authorization |
| --- | --- | --- |
| Debug | IPv4 loopback, port 5000 | No blanket authorization requirement; Swagger enabled |
| Release | IPv4 loopback, dynamically assigned port | Controllers require authorization |

[AppAuthenticationHandler](../../src/Infrastructure/Authentication/AppAuthenticationHandler.cs) requires both a loopback remote address and the application token in the `Authorization` header. The template-development token is accepted only under the template-development controller path. Loopback binding and token validation are separate checks; neither should be removed on the assumption that the other is sufficient.

Keep ProblemDetails mapping order and error contracts consistent with the frontend's interpretation of failures. Reporting responsibility at the API boundary is defined in [diagnostics](diagnostics.md#api-boundary).

## Single-instance contract

[InstancePipeName](../../src/Host/InstancePipeName.cs) identifies one instance per Windows session and user SID, independent of installation mode and elevation. Keep its application identifier, scope identifier and naming format stable across releases: changing them allows versions that share user data to run as independent owners.

[SingleInstanceServer](../../src/Infrastructure/SingleInstance/SingleInstanceServer.cs) arbitrates ownership through a named pipe with one server instance. It disconnects between clients without releasing the pipe name. Do not replace that with disposal and recreation between requests: another process could claim ownership in the gap.

Current activation limits in [BravoApplicationInstance](../../src/Host/BravoApplicationInstance.cs):

- A delivered payload does not guarantee that the requested document opened. Requests arriving before the window or WebView subscriptions are ready are not buffered.
- Activation callbacks can run concurrently and in a different order from arrival.
- Different elevation levels still share an instance name, but pipe access can prevent activation redirection. A redirect failure is logged and tracked without a user-facing dialog.

## Configuration boundaries

[PolicyService](../../src/Infrastructure/Policies/PolicyService.cs) exposes a snapshot initialized once. [RegistryPolicyReader](../../src/Infrastructure/Policies/RegistryPolicyReader.cs) merges machine and user policies, with configured machine values taking precedence. A null policy value means not configured; `false` is an explicit policy, not a request to use a default.

User preferences remain mutable through [UserPreferences](../../src/Infrastructure/Configuration/UserPreferences.cs). Do not assume that saving preferences reloads policies or that every consumer reapplies policy precedence. Host telemetry has different startup and update paths, documented in [telemetry consent](diagnostics.md#telemetry-consent).
46 changes: 27 additions & 19 deletions docs/design/code-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,41 @@ Rules that apply to code in this repository, beyond what `.editorconfig` enforce

## Files

New or rewritten `.cs` files are UTF-8 **with** BOM and use CRLF. This is not configured anywhere: it is what
every existing file under `src/` is, and `.editorconfig` sets `charset` only for project and resource files.
A tool that defaults to UTF-8 without BOM, or to LF, turns a small edit into a whole-file diff.
New or rewritten `.cs` files are UTF-8 **with** BOM and use CRLF. A tool that defaults to UTF-8 without BOM,
or to LF, turns a small edit into a whole-file diff.

## Naming and design

Follow the Microsoft .NET naming and design guidelines. Where a pattern already in the repository diverges
from them, the guidelines win: a local precedent is not an argument for repeating a mistake.

## Types

Assign primary constructor parameters to explicit `private readonly` fields. Do not reference a parameter
directly from the body of the type: an explicit field declares the dependency, its lifetime and its
mutability at the top of the type, where they can be read.
Follow the Microsoft [.NET naming guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines) for names and [framework design guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/) for type and member contracts. Read the relevant section when introducing or changing that kind of API. Where existing code diverges from the applicable guideline, follow the guideline rather than copying the divergence. Apply explicit formatting and field-naming rules from [.editorconfig](../../.editorconfig).

## Comments

Comment only what the code cannot express: origin, constraints, or rationale. Never restate the code. Document
the member, not its callers. Usage rules belong to the caller or to docs/. Keep summaries short and semantic
and exclude implementation details; put them in inline comments when they matter.
the member, not its callers. Usage rules belong to the caller or to docs/.

A `<summary>` is one sentence, third person, stating what the member is or does. Constraints and rationale go in
`<remarks>`. Implementation details go in inline comments, next to the code they explain.

Plain prose only, in comments and XML documentation alike: state the fact, do not perform it. A comment written
as a voice is copied in the same voice by the next reader, and the tone spreads faster than the content.

| Do not write | Write |
| --- | --- |
| "We safely allow the runtime to continue." | "The runtime continues." |
| "Catastrophic error while handling the exception." | "The handler failed while handling the exception." |
| "This ensures accurate crash dumps." | "The stack is intact at the throw site." |
| "There is nobody left to tell." | "The dialog is closed; the outcome is not reported." |

## Testability
Banned: first person; narration of the next statement; intensifiers and reassurance such as "safely", "simply",
"properly", "gracefully", "robust", "catastrophic"; metaphors and personification; rhetorical questions;
exclamation marks.

Design for testability from the start:
## Logging

- keep classes small and focused on one responsibility;
- inject dependencies through DI instead of relying on static state;
- pass external state — process, registry, filesystem, clock, network — through dependencies that tests can control.
A type that logs through `[LoggerMessage]` is `partial`, and the generated log methods live in a companion file
named `<TypeName>.Log.cs` next to the main file; the main file contains only the calls. Log methods inline in
the main file bury the flow of the type under attributes and message strings.

A type that can only be tested by running the application is not finished.
Each log method has its own `EventId`, unique within the type, so that an event can be filtered in the Event
Log without parsing the message. The level follows the outcome: `Warning` when the application continues unaffected,
`Error` when the operation visible to the user failed. Example: [BravoApplicationInstance.Log.cs](../../src/Host/BravoApplicationInstance.Log.cs).
Loading
Loading