Skip to content
Merged
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: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Clarify devcontainer setup steps in README

## Documentation Style Conventions

- **Carried files reference no other fleet repo unless that repo is the subject.** Every file the fleet carries verbatim - `AGENTS.md`, `CODESTYLE.md`, `WORKFLOW.md`, `README.md`, `.github/copilot-instructions.md`, the `repo-config/` and `spec/` files, the carried `AUDIT.md` - is **repo-agnostic**: it describes the shape or pattern self-containedly, names no sibling fleet repo as an illustrative example, and adds no link (inline URL or reference definition) to another fleet repo. This bans only *cross-repo* references - ordinary links to files within the same repo, to anchors, and to external sites are unaffected. Two reasons: any fleet repo may be **private**, so a cross-repo link 404s in a public carrier; and a cross-repo reference **couples** the repos and rots as they diverge. A repo name is allowed only when that repo *is* the subject (the file is about it). To point at a current good example, name it in the **onboarding/conformance issue** (one-off, not carried) or the hub-only `reports/conformance-matrix.md` (a fleet tracking table, not carried) - never in a carried doc. This pairs with the present-tense rule below: state the current shape, not a history of which repo it came from.

### Markdown

- **Reference-style links in human-facing docs.** Every markdown file **except** the agent-instruction files (`AGENTS.md` and `.github/copilot-instructions.md`, which optimize for agents and keep inline links) uses reference-style links only: every URI - internal path, anchor, external URL, or shield image - is defined at the **bottom of the file**, split into groups by type under an HTML-comment header (e.g. `<!-- Shields -->`, `<!-- Workflow -->`, `<!-- Repo -->`, `<!-- External -->`) with each group's definitions alphabetized by reference name. No inline `[text](uri)` targets in prose. **A URL inside a fenced code block stays inline** - reference links do not resolve in code blocks, so do not extract it, and exclude fenced code from any link-integrity check (bracket literals like `["a", "b"]` otherwise read as undefined references). **Removing a link also removes its reference definition** - an orphaned definition fails the no-unused-defs rule. The one exception is the Table of Contents, whose entries stay inline anchor links (see Table of Contents below).
Expand Down Expand Up @@ -139,7 +141,7 @@ Applies to code and workflow (`#`) comments alike.
- **[`.editorconfig`](./.editorconfig) sets the line ending:** `[*] end_of_line = crlf` is the **default** - every file type is CRLF unless pinned otherwise - with **LF** pinned for the execution-sensitive exceptions - `*.sh`, Dockerfiles, and any individual `.py` executed directly via its shebang (pinned **by path**, e.g. `spec/validate.py`; vanilla `.py` stays CRLF, since Python's universal newlines accept it and it is commonly edited on Windows). Only the LF exceptions are declared; the redundant per-type CRLF rules are intentionally omitted. `.gitattributes` mirrors it: `* -text` (git stores the exact bytes you commit and will **not** normalize) plus the matching LF pins.
- **Choosing an ending for a new file type:** CRLF is the **default** - cross-platform editors on Windows produce it, and it is harmless on Linux for everything except shell. Use LF only when the type **requires** it or CRLF **breaks how it is consumed**: executable scripts/shebangs (`*.sh`, s6, husky), Dockerfiles (CRLF breaks `RUN` heredocs/continuations), and tool-owned formats with a native LF ending (KiCad). **Non-workflow YAML stays CRLF** - GitHub Actions' parser tolerates it (a repo that also runs yamllint sets `new-lines: disable` to defer to `.editorconfig`). **Workflow YAML (`.github/workflows/*.{yml,yaml}`) is pinned LF** in `.editorconfig` - Dependabot and Actions rewrite it with LF, so declaring LF keeps it consistent instead of mixed on every bump. This (and the catalog snippet workflows in `catalog/snippets/workflows/*`, pinned LF the same way) is an LF class **not** backed by a `.gitattributes` pin: git keeps `* -text` (no normalization), and CI's `editorconfig-checker` (EOL-only) catches a mismatch instead. Distinguish where a file is *consumed* from where it is *edited*: consumption on Linux alone does not force LF. A config or pattern file consumed by a Linux tool stays CRLF when the tool tolerates a trailing CR: `.dockerignore` and `.gitignore` are CRLF (their parsers strip the CR), and only a *Dockerfile* - interpreted, where a CR breaks `RUN` heredocs and line continuations - is LF.
- **Operational (config) repos: the global default follows the consuming application's native platform, not the fleet CRLF default.** A config repo (registry `workflowModel: operational`) is a *view into an application's configuration directory* - often the exact tree mounted into that app's container - so its files must use the ending the app itself reads and writes, and forcing the fleet CRLF default would fight the app. Set the `[*] end_of_line` default to the app's native ending and record it in the registry [`lineEndings`](./registry/repos.json) field (`lf` | `crlf`): **LF** for a Linux-native app whose config lives in a Linux container - ESPHome, Home Assistant, a devcontainer-only or HACS config - and **CRLF** for a Windows-native editor - e.g. Vantage InFusion config edited by Design Center on Windows. The execution-sensitive LF pins (`*.sh`, Dockerfiles, workflow YAML) still apply on top, and `.gitattributes` still mirrors the chosen default. This override is for operational repos only; `release` repos keep the `[*] end_of_line = crlf` fleet default above. Do **not** re-normalize such a repo to the fleet default - that is exactly the over-normalization these per-repo endings prevent.
- **Mixed-consumer config: prefer to split by platform into single-platform repos, not one mixed repo.** When a config repo would be consumed on two platforms (a Linux app plus a Windows-edited subtree), the clean answer is a repo per consumer, each single-platform with its own `lineEndings` - e.g. the Vantage InFusion / Design Center config (Windows/CRLF) lives in its own `Vantage-Config` repo, **not** as a `Vantage/**` subtree inside the Linux-`lf` `HomeAutomation-Config`. That keeps each repo's default, CI, and checkout matched to one platform and avoids per-path EOL machinery entirely. **Fallback only if a subtree genuinely cannot be split out:** keep the global default at the primary consumer and pin the odd subtree with an `.editorconfig` path override (e.g. `[<subtree>/**] end_of_line = crlf`) matching its consumer, treated like any tool-owned format; the global `* -text` in `.gitattributes` already preserves those bytes, so no extra git pin is needed.
- **Mixed-consumer config: prefer to split by platform into single-platform repos, not one mixed repo.** When a config repo would be consumed on two platforms (a Linux app plus a Windows-edited subtree), the clean answer is a repo per consumer, each single-platform with its own `lineEndings` - e.g. a controller config edited by a Windows-native editor (CRLF) lives in its own repo, **not** as a subtree inside a Linux-`lf` config repo. That keeps each repo's default, CI, and checkout matched to one platform and avoids per-path EOL machinery entirely. **Fallback only if a subtree genuinely cannot be split out:** keep the global default at the primary consumer and pin the odd subtree with an `.editorconfig` path override (e.g. `[<subtree>/**] end_of_line = crlf`) matching its consumer, treated like any tool-owned format; the global `* -text` in `.gitattributes` already preserves those bytes, so no extra git pin is needed.
- **Scripts and extensionless executables must be LF - and pinned in `.gitattributes`, not just configured.** A CRLF shebang (`#!/usr/bin/env bash\r`) breaks execution. `.editorconfig` sets `[*.sh] = lf`, but that extension-based rule does not match **extensionless** executables (s6 service scripts `run`/`up`/`finish`, husky/git hook scripts like `.husky/pre-commit`), and `* -text` enforces nothing - so a broad normalization pass or an editor can silently flip them to CRLF (it has). `.gitattributes` is the enforcement layer: it carries `*.sh text eol=lf`, and any repo whose tooling ships extensionless scripts **adds the matching path pin** - e.g. `Docker/s6-overlay/** text eol=lf` for s6 init, `.husky/pre-commit text eol=lf` for husky hooks - so git holds them at LF on checkout and `--renormalize`. This pin is mandatory for any repo that overrides s6 init, uses husky/git hooks, or otherwise ships executable scripts. The same explicit-pin rule extends to **tool-owned file formats the base config doesn't key on**: pin them to whatever ending the tool reads and writes so a normalization sweep can't churn them - e.g. KiCad project/footprint/3D files (`*.kicad_mod`, `*.kicad_sym`, `*.step`), which KiCad writes LF (`*.kicad_mod text eol=lf`, ...). The principle is general: a file class the `.editorconfig` extension rules and `* -text` don't cover needs an explicit `.gitattributes` pin matching its tool's native ending.
- **Pair each such pin with a matching `.editorconfig` override - the git pin alone is not enough.** `.gitattributes` governs **git** (checkout, commit, `--renormalize`); the **editor** follows `.editorconfig`, where the `[*] end_of_line = crlf` default still applies to any file no extension rule covers. So even with the git pin, the editor writes a CRLF shebang into an extensionless hook (breaking it when run from the working tree) or re-ends/trims a byte-sensitive data file. Give every extensionless **executable** an editorconfig LF override beside its `.gitattributes` pin (`[.husky/pre-commit] end_of_line = lf`); and for a **byte-preserve data directory** (downloaded or opaque source whose exact bytes the consumer may depend on) disable *all* editor normalization, not just EOL - `[<dir>/*]` with `charset = unset`, `end_of_line = unset`, `insert_final_newline = false`, `trim_trailing_whitespace = false` (`unset` is EditorConfig's spec-defined special value that removes an inherited property, so the editor enforces neither the global `charset` nor `end_of_line` on that path). Keep these overrides with the line-ending governance (above any `.NET-only` divider), not in the language-style section.
- **New files:** create them with the `.editorconfig`-mandated ending.
Expand Down
6 changes: 3 additions & 3 deletions CODESTYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions][analyzer-d
logger.LogError(exception, "{Function}", function);
```

2. **Libraries log through abstractions, never a concrete backend.** A NuGet **library** depends only on `Microsoft.Extensions.Logging.Abstractions` and exposes an `ILoggerFactory` seam - a settable global factory defaulting to `NullLoggerFactory.Instance` (fallback `NullLogger.Instance`) with `SetFactory`/`TrySetFactory`, and/or an `ILoggerFactory`/`ILogger` parameter in its API. It must **not** reference Serilog or any sink - that forces a logging framework on every consumer and drags in AOT-incompatible dependencies. The consuming **application** owns the concrete logger (Serilog is fine there), bridges it to `ILoggerFactory` (e.g. `SerilogLoggerFactory` from `Serilog.Extensions.Logging`), and injects it. Reference: `LanguageTags` - `LogOptions` in the library; the CLI's `LoggerFactory` builds the Serilog-backed factory and injects it via `LogOptions.SetFactory`.
2. **Libraries log through abstractions, never a concrete backend.** A NuGet **library** depends only on `Microsoft.Extensions.Logging.Abstractions` and exposes an `ILoggerFactory` seam - a settable global factory defaulting to `NullLoggerFactory.Instance` (fallback `NullLogger.Instance`) with `SetFactory`/`TrySetFactory`, and/or an `ILoggerFactory`/`ILogger` parameter in its API. It must **not** reference Serilog or any sink - that forces a logging framework on every consumer and drags in AOT-incompatible dependencies. The consuming **application** owns the concrete logger (Serilog is fine there), bridges it to `ILoggerFactory` (e.g. `SerilogLoggerFactory` from `Serilog.Extensions.Logging`), and injects it. Reference pattern: a `LogOptions` seam in the library; the consuming CLI builds the Serilog-backed factory and injects it via `LogOptions.SetFactory`.

3. **CallerMemberName**: Use for automatic function name tracking

Expand Down Expand Up @@ -354,8 +354,8 @@ This is the style guide for any **Python project(s)** in this repo.

**Two profiles.** A repo's Python is one of two shapes, and the rest of this section (uv project, `uv.lock`, `uv run`, `src` layout, pytest coverage) describes the **project** profile. The two differ by whether the Python has **third-party runtime dependencies**, which shows up structurally in `pyproject.toml`, so the audit detects the profile there (`python.profile.detect`):

- **Project** - the Python has third-party runtime dependencies, or is the repo's deliverable. It is a PEP 621 uv project: `[project]` with `dependencies` (dev tools in `[project.optional-dependencies]` or `[dependency-groups]`), a `[build-system]`, and a committed `uv.lock` (pinned LF - see [Line Endings][line-endings]); CI runs `uv sync --frozen` + `uv run <tool>`, so the lockfile pins tool versions. Financial-Modeling is the reference.
- **Scripts** - stdlib-only utility scripts embedded in a **non-Python** repo (e.g. the Python tooling subtree of a `csharp` app - PlexCleaner's `RegressionTests/`). Run the tools with **`uvx`** (no project install, no lockfile): the `pyproject.toml` carries **only** `[tool.ruff]` / `[tool.mypy]` config - no `[project]`, no `[build-system]`, no `uv.lock` (that metadata would misrepresent it as a shippable package). **mypy** is the type checker (there is no first-party package for pyright strict to anchor on). Because there is no lockfile to pin versions, **CI pins the exact tool versions in the `uvx` command** (`uvx ruff@<ver>`, `uvx mypy@<ver>`, bumpable there) while the VS Code tasks and README run the unpinned latest - a deliberate CI-vs-local gap so local tooling never silently falls behind. `.py` files follow the repo's line-ending default (CRLF in a CRLF-default repo; a shebang-executed script is LF-pinned by path - see [Line Endings][line-endings]). There is no pytest suite, so the coverage expectation is N/A; a co-present `csharp` type still carries `codecov.yml` for its own tests.
- **Project** - the Python has third-party runtime dependencies, or is the repo's deliverable. It is a PEP 621 uv project: `[project]` with `dependencies` (dev tools in `[project.optional-dependencies]` or `[dependency-groups]`), a `[build-system]`, and a committed `uv.lock` (pinned LF - see [Line Endings][line-endings]); CI runs `uv sync --frozen` + `uv run <tool>`, so the lockfile pins tool versions.
- **Scripts** - stdlib-only utility scripts embedded in a **non-Python** repo (e.g. a Python tooling subtree of a `csharp` app). Run the tools with **`uvx`** (no project install, no lockfile): the `pyproject.toml` carries **only** `[tool.ruff]` / `[tool.mypy]` config - no `[project]`, no `[build-system]`, no `uv.lock` (that metadata would misrepresent it as a shippable package). **mypy** is the type checker (there is no first-party package for pyright strict to anchor on). Because there is no lockfile to pin versions, **CI pins the exact tool versions in the `uvx` command** (`uvx ruff@<ver>`, `uvx mypy@<ver>`, bumpable there) while the VS Code tasks and README run the unpinned latest - a deliberate CI-vs-local gap so local tooling never silently falls behind. `.py` files follow the repo's line-ending default (CRLF in a CRLF-default repo; a shebang-executed script is LF-pinned by path - see [Line Endings][line-endings]). There is no pytest suite, so the coverage expectation is N/A; a co-present `csharp` type still carries `codecov.yml` for its own tests.

### Toolchain

Expand Down
6 changes: 1 addition & 5 deletions repo-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Every fleet repo carries this directory; the hub keeps the canonical copy. Rules

- **Carry only your model's `develop` variant.** A `release` repo carries `develop.json`; an `operational` repo carries `operational/develop.json` instead. `main.json` and `settings.json` are shared by both models. `configure.sh` aborts when the payload its model needs is missing rather than applying a partial configuration.
- **Hub-only references stay plain text.** The hub is a private repo: never URL-link it from a downstream repo - the link 404s for anyone without hub access. Files whose canonical fleet-wide form lives only at the hub are mentioned by name, not linked; links into files every repo carries (`AGENTS.md`) resolve everywhere and are fine.
- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline - the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative, and the local copies never link the hub. The reference adaptation is the [Vantage-Config carry][vantage-config] (`AUDIT.md` + `spec/secrets.json`, operational model): a settings diff, a normalized ruleset diff against the carried payloads, and a names-only secrets check, all targeting the current repo - adapt it, don't invent. A `release` repo adapts the same shape: its `develop` payload stays at `repo-config/develop.json`, and its `spec/secrets.json` keeps the baseline App pair plus the secret names for its own publish mechanisms (from the hub's canonical `spec/secrets.json`). The fleet audit letter-checks both carried files (`spec/files.json`).
- **Adapted self-audit carry.** A downstream repo carries **locally adapted** `AUDIT.md` and `spec/secrets.json`, scoped to self-auditing its own rulesets, settings, and secrets against the committed `repo-config/` baseline - the standard shape, so the carried tooling is self-contained. The hub's fleet-wide audit remains authoritative, and the local copies link no other repo. The adapted `AUDIT.md` is a settings diff, a normalized ruleset diff against the carried payloads (an operational carry swaps in `operational/develop.json`), and a names-only secrets check, all targeting the current repo - adapt this shape, don't invent. A `release` repo keeps its `develop` payload at `repo-config/develop.json`, and its `spec/secrets.json` keeps the baseline App pair plus the secret names for its own publish mechanisms (from the hub's canonical `spec/secrets.json`). The fleet audit letter-checks both carried files (`spec/files.json`). A current well-formed example is named in the onboarding/conformance issue, not linked here.
- **The regen snippet targets the current repo**, so it works unchanged in a carried copy.

## Rulesets
Expand Down Expand Up @@ -72,7 +72,3 @@ The fleet-standard general settings live in [`settings.json`][settings-json] and
[agents-branching-model]: ../AGENTS.md#branching-model
[agents-git-and-commit-rules]: ../AGENTS.md#git-and-commit-rules
[settings-json]: ./settings.json

<!-- External -->

[vantage-config]: https://github.com/ptr727/Vantage-Config
Loading