D001-D021 become ADR 0001-0021 in docs/decisions/, one decision per file. D017's PESTER_VERSION amendment was a second decision in one entry and becomes ADR 0022. ADR 0023 records the migration and reverses the lane2-change-plan ruling that deliberately kept DECISIONS.md until the CI lane work landed. DECISIONS.md is reduced to a stub with a D-to-ADR redirect table, so the four released CHANGELOG entries and older issue bodies that cite it degrade to a redirect rather than a dead reference. docs/review/ and docs/lane2-change-plan.md are deleted (ADR 0024). Of 91 findings, 83 were resolved and six of the seven still open were already GitHub issues; F021 was the exception and is now #130. CLAUDE.md's Key Conventions list gains the two rules it was missing and becomes the checklist, with the ADRs carrying rationale.
1.4 KiB
ADR 0003 — URL encoding required for all path parameters
- Status: Accepted
- Date: 2026-03-22
- Deciders: unrecorded; adopted during review scan 2026-03-22
- Context source:
docs/review/findings.jsonF050
Context
Snapshot names, node names, user IDs and similar identifiers were interpolated raw into API URL paths. Most reach the module from validated sources, but nothing in the code enforced that, and a value carrying / or ? would silently change which endpoint was called.
Decision
Every user-supplied or dynamic value interpolated into an API URL path is wrapped in Uri.EscapeDataString(). This applies to all service classes without exception.
var resource = $"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapshotName)}";
Rejected alternatives
Encoding only the parameters that can plausibly carry a separator, and trusting validation upstream for the rest. Rejected because the audit then has to be redone on every new call site, and the reader cannot tell a deliberate omission from an oversight:
var resource = $"nodes/{node}/qemu/{vmid}/snapshot/{snapshotName}";
Consequences
Applied across all 14 service classes at the time of the decision. Form-encoded bodies are a separate matter — PVE does not URL-decode form values in some internal consumers, so the cluster-join path deliberately sends minimally encoded values.