diff --git a/CLAUDE.md b/CLAUDE.md index c17aab7..8e0625f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,8 +14,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **Abstracted from:** Customer-specific `./vault` CLI (bash) and `credentials-ui` (FastAPI) into a generalized open-source tool. -**Landschaften:** [vaultctl](docs/landscapes/vaultctl/LANDSCAPE.md) — design decision records and cross-cutting design notes live here. - ## Build and Development Commands ```bash diff --git a/docs/assets/cdds-logo.png b/docs/assets/cdds-logo.png new file mode 100644 index 0000000..a741a4e Binary files /dev/null and b/docs/assets/cdds-logo.png differ diff --git a/docs/build_manual.sh b/docs/build_manual.sh index 7c7b212..fa22516 100755 --- a/docs/build_manual.sh +++ b/docs/build_manual.sh @@ -5,6 +5,10 @@ # via the system package manager (`pandoc`, `texlive-xetex`, plus the Noto and # DejaVu font packages). All settings (fonts, layout, classoptions) live in # the YAML metadata block at the top of manual.md — no separate template. +# +# The vaultctl version is read from ../pyproject.toml at build time and +# injected into the manual's subtitle, so the rendered PDF always carries +# the version it documents. set -euo pipefail @@ -19,9 +23,17 @@ if ! command -v xelatex >/dev/null 2>&1; then exit 1 fi +# Extract `version = "X.Y.Z"` from the [project] section of pyproject.toml. +VERSION="$(grep -E '^version\s*=' ../pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" +if [[ -z "${VERSION}" ]]; then + echo "Error: could not extract version from ../pyproject.toml" >&2 + exit 1 +fi + pandoc manual.md \ --pdf-engine=xelatex \ --highlight-style=tango \ + --metadata=subtitle:"User Manual · vaultctl v${VERSION}" \ --output=manual.pdf -echo "Built: $(pwd)/manual.pdf" +echo "Built: $(pwd)/manual.pdf (vaultctl v${VERSION})" diff --git a/docs/landscapes/vaultctl/LANDSCAPE.md b/docs/landscapes/vaultctl/LANDSCAPE.md deleted file mode 100644 index ea77509..0000000 --- a/docs/landscapes/vaultctl/LANDSCAPE.md +++ /dev/null @@ -1,45 +0,0 @@ -# vaultctl Landscape - -vaultctl is a single-repo tool — there is no cross-repo landscape in the -client-engagement sense. This directory exists because the tool has -accumulated several cross-cutting design decisions worth documenting in -a structured way, separate from CLAUDE.md (which describes the -*current* state) and PR descriptions (which describe *changes*). - -The convention follows the Architecture Decision Records (ADR) pattern -for `decisions/`. Subdirectories `patterns/` and `initiatives/` may be -added later if needed. - -## Decisions - -| # | Title | Status | -|---|-------|--------| -| [0001](decisions/0001-schema-inference-via-python-walker.md) | Schema inference via Python walker, not `cue import` | Accepted | - -## Why a Landscape (Note on Convention) - -Per the global Claude Code guideline, tool repos typically don't have a -landscape — landscapes group cross-repo work for client engagements. -vaultctl is an exception by virtue of size and decision density: the -choice to stay in Python over Go/Rust, the subprocess pattern for -`ansible-vault` and `cue`, the project-local `.vaultctl/` layout, the -schema inference approach — these are all decisions that future -contributors (including the user's future self) will benefit from -seeing in one place. - -If the convention should win, the directory can collapse to flat -`docs/decisions/` later. The ADR files would migrate unchanged. - -## Backlog of Decisions to Backfill - -When relevant, these existing-but-undocumented decisions should be captured: - -- Python over Go/Rust for the implementation language (#36 era — covered - briefly in CLAUDE.md but not as an explicit ADR). -- CUE validation via `cue` binary subprocess instead of native Python - binding (#34 — rationale lives in the issue body and PR #39 description). -- Project-local `.vaultctl/` directory layout, no backward compatibility - with the legacy root-level `.vaultctl.yml` (#37/PR #38). - -These remain in their PR descriptions and CLAUDE.md sections for now — -backfill is a P4 chore. diff --git a/docs/landscapes/vaultctl/decisions/0001-schema-inference-via-python-walker.md b/docs/landscapes/vaultctl/decisions/0001-schema-inference-via-python-walker.md deleted file mode 100644 index 40471b5..0000000 --- a/docs/landscapes/vaultctl/decisions/0001-schema-inference-via-python-walker.md +++ /dev/null @@ -1,113 +0,0 @@ -# 0001 — Schema inference via Python walker, not `cue import` - -- **Status:** Accepted -- **Date:** 2026-04-27 -- **Decision in:** [PR #41](https://github.com/cdds-ab/vaultctl/pull/41) -- **Related:** [#34](https://github.com/cdds-ab/vaultctl/issues/34), [#40](https://github.com/cdds-ab/vaultctl/issues/40) - -## Context - -`vaultctl schema infer` derives a CUE schema baseline from the current -vault content. The decision was how to implement the YAML-to-CUE-schema -transformation: - -1. **Option A — Use the `cue import` subcommand:** shell out to `cue import data.yml`, - then post-process the resulting CUE to replace concrete values with - their types. -2. **Option B — Pure-Python walker:** walk the decrypted vault dict - directly and emit CUE syntax with types in place of values. - -The asymmetry is critical: `cue import` produces *concrete data in CUE -syntax*, not a *schema*. A schema is what we want. - -```yaml -# Input -username: admin -``` - -```cue -// `cue import` output (data, not schema) -username: "admin" - -// What schema infer must produce -username: string -``` - -So Option A still needs a value-to-type substitution pass. The bulk of -the work is the substitution pass; running `cue import` first only adds -a subprocess and a parse-and-rewrite step around it. - -## Decision - -Use Option B — pure-Python walker. Map Python types directly to CUE -types as the dict is traversed. - -| Python type | CUE type emitted | -|-------------|------------------| -| `bool` (checked before `int`!) | `bool` | -| `str` | `string` | -| `int` | `int` | -| `float` | `number` | -| `None` | `null` | -| `list[T]` (homogeneous) | `[...T]` | -| `list` (mixed types) | `[...(T1 \| T2 \| ...)]` (sorted disjunction) | -| `dict` | nested struct, recursively | -| anything else | `_` (CUE top type) — surfaces unexpected shapes without crashing | - -`_previous` backup keys are excluded; field names with non-identifier -characters get quoted. - -## Consequences - -**Positive:** - -- Self-contained: no extra `cue` subprocess per inference. Schema - generation works without the `cue` binary on `$PATH` (though - `validate` still needs it for the round-trip check). -- Testable in pure Python: 11 unit tests run without any external - dependency. Round-trip tests then exercise the inferred schema - against `cue vet` to confirm the output is valid CUE. -- Output is deterministic (sorted keys, stable formatting), suitable - for git-tracking and diffs. - -**Negative:** - -- We don't get CUE's parser for free. Edge cases like malformed YAML - surface as Python errors, not CUE errors. Acceptable — `decrypt_vault` - already raises before the walker runs, and YAML parse errors come - from `pyyaml`, which is the upstream of vault content anyway. -- If we ever want to consume someone else's CUE input (e.g., a JSON - Schema imported via `cue import --jsonschema`) and turn that into - vaultctl's schema shape, we'd need a different code path. Not - currently a use case — only relevant if vaultctl ever ingests - external schema definitions. - -**Neutral:** - -- The walker emits closed schemas (`#VaultFile: { ... }` with no `...` - at the end). This is intentional: a closed inferred schema means - adding a new vault key without re-running `infer` (or extending the - schema by hand) makes `vaultctl validate` fail. That is the value — - structural changes become deliberate steps. - -## Alternatives Considered - -- **`cue import` + AST post-processing**: rejected. Adds a subprocess - hop and a CUE-to-CUE rewrite layer for no extra capability — the - type-substitution logic is the same effort either way. -- **Native Python CUE binding**: rejected. No production-ready binding - exists as of April 2026 (`pycue` is alpha and inactive, official - `cue-py` not released — see [#34](https://github.com/cdds-ab/vaultctl/issues/34) - for the survey). -- **JSON Schema generation, then `cue import --jsonschema`**: rejected. - Adds two indirections (Python→JSON Schema→CUE) for no benefit, and - loses information about CUE-specific constraints (regex, time formats) - that we may want to add later. - -## References - -- Implementation: `src/vaultctl/schema.py` — `infer_vault_schema()`, - `_render_type()`, `_quote_field()`. -- Tests: `tests/test_schema.py` — `test_infer_*` (unit) and - round-trip tests with the `requires_cue` marker. -- Discussion: PR #41 description and conversation history. diff --git a/docs/manual.md b/docs/manual.md index a7ec37f..a3454de 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -33,6 +33,7 @@ header-includes: - \usepackage{microtype} - \usepackage{fancyhdr} - \usepackage{xcolor} + - \usepackage{graphicx} - \definecolor{codebg}{HTML}{F4F4F2} - \usepackage{fvextra} - \DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,commandchars=\\\{\}} @@ -40,6 +41,8 @@ header-includes: - \renewcommand{\sfdefault}{\familydefault} - \addtokomafont{disposition}{\rmfamily} - \setkomafont{descriptionlabel}{\rmfamily\bfseries} + - \titlehead{\centering\includegraphics[width=3.5cm]{assets/cdds-logo.png}} + - \publishers{\normalsize cdds-ab · \texttt{github.com/cdds-ab/vaultctl}} --- # Introduction diff --git a/docs/manual.pdf b/docs/manual.pdf index 1412f52..ce26f09 100644 Binary files a/docs/manual.pdf and b/docs/manual.pdf differ