Files
qctl/docs/design/codegen-locks.md
james.maes f751b2eb4e refactor: reorganize repository structure
- Move design docs to docs/
- Move packaging files to packaging/
- Create GitHub issues and milestones for tracking
- Update workflow and README references
2025-12-31 06:23:24 -06:00

182 lines
7.8 KiB
Markdown

## V1 Code-Gen Locks (finalized)
Use this as the source of truth to scaffold modules, POMs, deps, and initial code.
### Build baseline
- Java: Temurin 21.0.x (LTS)
- Maven: 3.9.7 (wrapper pinned)
- GraalVM: 21.0.2 (matching JDK 21)
- GroupId / base package: `io.qrun.qctl` / `io.qrun.qctl.*`
- Modules:
- root POM (reactor)
- `qctl-shared` (DTOs, utils)
- `qctl-core` (config, logging, http, auth, telemetry, cache)
- `qctl-qqq`, `qctl-qbit`, `qctl-qrun`, `qctl-qstudio` (commands)
- `qctl-integration-tests` (Testcontainers, golden tests)
- Toolchain (root `pom.xml`):
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchain-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<toolchains>
<jdk>
<version>21</version>
<vendor>eclipse</vendor>
</jdk>
</toolchains>
</configuration>
</plugin>
```
### Dependency matrix (pin exact versions)
- Picocli: 4.7.5 (+ `picocli-codegen` for completion/man)
- Jackson: BOM 2.17.1 (core, databind, dataformat-yaml)
- JSON Schema: `com.networknt:json-schema-validator:1.4.1`
- HTTP: JDK HttpClient (`java.net.http`) — no OkHttp in V1
- Templating: `com.github.jknack:handlebars:4.4.0`
- PGP: `org.bouncycastle:bcpg-jdk18on:1.78.1`
- SLF4J: API 2.0.13; backend Logback 1.5.6 (JSON via jackson encoder)
- Retry/backoff: homegrown (tiny util in `qctl-core`)
- Tests: JUnit 5.10.2, AssertJ 3.25.3, Mockito 5.12.0, Testcontainers BOM 1.19.7
- Code quality: Spotless 2.43.0 (GJF 1.17.0), Checkstyle 10.15.0, Error Prone 2.28.0 (via EP Maven plugin)
- Native: `native-image-maven-plugin` 22.3+ (cfg per-OS)
### Package & project structure
- `qctl-shared`: ULID, RFC7807 types, small I/O utils, path/glob utils, SemVer helper
- `qctl-core`:
- Config loader/merger + env overlay
- Logger setup & output formatters
- HTTP client + retry/jitter + auth header injector
- Auth device-flow stub + token store abstraction
- Telemetry writer
- Cache/LRU + integrity utilities
- Feature modules (`qctl-qqq`, `qctl-qbit`, `qctl-qrun`, `qctl-qstudio`) expose Picocli commands; depend on `qctl-core` and `qctl-shared`
- `qctl-integration-tests`: launches mock (Prism), runs golden/contract tests
### Config implementation details
- Discovery/merge order (as defined): built-in < global < project < env `QCTL_*` < flags; later wins
- Error messages: JSON Pointer path + short reason
- Example: Config error at `/qbit/resolution/preferLatestMinor`: expected boolean, got "yes"
- `--output` precedence: flag overrides config for the invocation only
- Env vars: `QCTL_` prefix, uppercase keys with `_` mapping to nested fields (e.g., `QCTL_QRUN_DEFAULTENV=stage`)
- Booleans: true if {1,true,yes,on}; false if {0,false,no,off} (case-insensitive)
- Arrays: JSON array or comma-separated with trimming
- URI fields validated per schema `format: uri`
### Logging & output
- Facade: SLF4J; backend: Logback
- Levels: default INFO; `--debug` => DEBUG; `--verbose` adds CLI prints (does not raise logger level unless combined with `--debug`)
- Formats:
- Text: `[time] level module - msg` with optional color
- JSON: one-line JSON (logstash-logback-encoder)
- Color: auto = enabled on TTY and `NO_COLOR` not set; `always`/`never` obeyed
- Streams: logs → stderr; command results (per `--output`) → stdout
### HTTP layer
- Client: `java.net.http.HttpClient` (HTTP/2 on)
- Timeouts: connect 5s, request 30s; `--timeout` overrides
- Retries: for 429/5xx and IOExceptions; max 3; decorrelated jitter backoff (base 250ms, cap 5s); honor `Retry-After`
- Headers: Idempotency-Key (new ULID per POST unless provided), `User-Agent: qctl/<ver> (<os>/<arch>)`, auth header
- Errors: parse RFC7807 into ProblemDetail
### Auth V1 behavior
- Primary: OIDC Device Flow (stub prints code & polls)
- Token store: OS keychain; fallback file store `~/.config/qctl/tokens.json` (0600)
- Mock override: if `endpoints.api` is `http://localhost:*` or `QCTL_API_KEY` is set → send `X-API-Key`, skip OIDC
- Precedence: `X-API-Key` > OIDC token > none
- `whoami`: token claims (OIDC) or `api-key: set`
### Error → exit mapping
- HTTP ProblemDetails mapping: 401/403 → 4, 404 → 5, 400/422 → 6, 409/412 → 8, 408/429/5xx + IO → 3
- Integrity/signature (local) → 7; CLI validation → 2; unexpected → 1; Ctrl-C → 9
### qqq (templates)
- Handlebars: enable partials & helpers; built-ins: upper, lower, camel, snake, kebab, json, env
- Manifest (template.yaml) minimal fields:
```yaml
id: web-basic
version: 1.2.0
description: Basic web app starter
prompts: [] # optional interactive questions
postGen: [build, run, healthcheck] # overrides config if present
files: {} # mapping or glob rules if needed (V1: simple copy)
```
- Local path rules: `--template PATH` supports dir or `.zip`; must contain `template.yaml`
- Post-gen hooks: phases (build/run/healthcheck), each as command array, inherited env; stream output; non-zero fails phase but not generation; print guidance
### qbit (packages)
- Lockfile path: `qbits.lock` (project root)
- Write semantics: atomic via temp + `Files.move(ATOMIC_MOVE)`; include `generatedAt`
- File locking: advisory via `FileChannel.tryLock()`; helpful wait/fail message
- Vendor layout: `vendor/qbits/<group>/<name>`; tarball expanded there
- Resolution:
- hermetic: only lockfile entries used; fetch exact resolved URLs permitted; no registry search
- offline: no network ever; must already be in cache or vendor
- Integrity: compute sha512 of downloaded tarball; compare to lockfile integrity
- Mismatch (V1): warn (do not fail)
- Signatures: if present, verify PGP → warn only on failure
### qrun (package/publish)
- Build: Jib Core (programmatic) to build OCI without invoking a Maven goal
- Labels: inject frozen OCI annotations; prevent overwrite in V1
- Registry auth: detect from `~/.docker/config.json` (credHelpers or auths); else anonymous
- Verify-by-digest: after push, HEAD `v2/<name>/manifests/<digest>` (OCI Accept); 200 OK; 401/404 → exit 6 if local metadata says it should exist, else 3 if network issue (heuristics by exception)
### qstudio (offline)
- Ledger: `.qctl/ledger/` in project; filename `${ulid}.json`
- Plan output: `plan.md` at project root unless `--out`
- Globs: PathMatcher + `glob`/`**` patterns; allow/deny applied in order, deny wins
### Cache
- Roots:
- macOS `~/Library/Caches/qctl/`
- Linux `$XDG_CACHE_HOME/qctl/` or `~/.cache/qctl/`
- Windows `%LOCALAPPDATA%\qctl\cache\`
- Subdirs: `templates/`, `qbits/`, `artifacts/`, `trust/keys/`, `http/`
- LRU index: `index.json` (array of {path,size,atime}); lazy recompute if missing
- Prune: `--max-size` with units (KB/MB/GB); evict oldest atime first; print before/after
- Clean `--all`: deletes cache root; confirm unless `--yes`
### Telemetry (opt-in)
- Gate: config or `--telemetry.enabled`
- Storage: `$configDir/telemetry/events.log` (rotate at 5MB, keep 3)
- Write on process exit; failures are silent
- Schema: minimal fields per DESIGN-2.md; no network in V1
### OpenAPI integration
- Client: hand-written minimal DTOs (POJOs in `qctl-shared`) + small `ApiClient` in `qctl-core`
- DTO packages: `io.qrun.qctl.shared.api.*`
### CLI UX details
- Structure: `qctl <group> <verb>` where group matches module
- Help: Picocli usage; `qctl man` generates man pages via codegen plugin
- Completion: picocli-codegen outputs bash/zsh/fish/pwsh scripts; `qctl completion <shell>` prints install instructions
### Testing
- Golden snapshots: `qctl-integration-tests/src/test/resources/golden/`; update via `-Dgolden.update=true`
- Contract tests: start Prism on port 4010; validate schemas via networknt; assert headers like `X-Total-Count`
- Simulated errors: Use an additional WireMock for 401/404/429/503 and timeouts to test retry/backoff and exit codes
- CLI golden: run via `ProcessBuilder`, capture stdout/stderr, compare to snapshots (normalize timestamps/ULIDs)