tgwrap — Architecture & Quality Assessment

Python CLI wrapping Terragrunt / OpenTofu · v1.0.10 · commit 0ba3c09 (main) · assessed 2026-10-18
Run #1 (baseline) · Assessor: Claude Code (Opus 5.5)

Executive summary

C
Overall: C — Acceptable, notable gaps
tgwrap is a mature, useful, actively maintained tool with sensible safety defaults. The grade is held back by thin automated verification (30% coverage, CI runs tests only), 34 known CVEs in locked transitive dependencies, a failure path in deploy that exits 0, and leaky layering (CLI concerns inside services).
Business & risk framing
  • tgwrap sits in the change path to production infrastructure. When it fails silently or misreads a failure, the cost is an outage or an unnoticed drift, not a cosmetic bug.
  • The safety rails are good: auto-approve applies only to apply/destroy and has a spec; deletions are guarded by terrasafe; state is locked by default; no shell=True anywhere.
  • The verification rails are weak: orchestration code (graph_runner, main, cli, analyzer, azure) has 0% test coverage, and lint, type and security gates are configured but not enforced.
  • Most fixes are cheap. Roughly 1–2 engineer-days of quick wins would move the grade to B−.
Top risk
Sync failure exits 0 high
tgwrap/deploy.py:35-47
Top risk
34 CVEs in lockfile high
pyjwt, urllib3, cryptography, idna
Top risk
Orchestration untested med
graph_runner / main / cli at 0%
Top risk
String-built argv med
unquoted paths, git clone without --

Scorecard

N/A means the repo has no artifacts of that type. That is treated as a deliberate scope decision and does not lower the overall grade. tgwrap is a CLI that operates Terragrunt/Terraform code but contains none itself, so dimension 3 is assessed as IaC orchestration safety.

C
1 · Architecture & Service Design
Façade plus services is a sound shape, but the layering leaks: services call sys.exit and raise click exceptions, legacy function modules sit alongside the services, and three core modules are orphaned.
C
2 · Python Engineering Quality
Pylint scores 9.72, and every HTTP call has a timeout and uses safe YAML loading. Against that: 303 ruff findings, 165 mypy errors under a strict config, 14/20 files unformatted, and process-global side effects (os.chdir, os.environ).
B
3 · IaC Orchestration Safety reframed
Approval scoping is spec'd, the terrasafe deletion guard, drift scoring and the version gate (≥0.88) are present, and state locking is on by default. Weak spot: module failure is detected by matching "error" in stderr.
N/A
4 · Kubernetes & Platform
No Kubernetes artifacts, as expected for a CLI tool.
C
5 · Operational Excellence (CLI/CI lens)
Telemetry goes to an Azure DCE and the step-resume feature is useful. Exit codes are inconsistent, the structured logger is unused, there are no retries, and every run makes a PyPI call.
C
6 · Azure Architecture Review
Uses DefaultAzureCredential with minimal token scopes and doesn't log tokens. But there are two auth paths (az CLI vs SDK), no pagination or 429 retry, and API versions as old as 2014.
N/A
7 · AWS Architecture Review
No AWS integration.
B
8 · Multi-Cloud & Portability
The run/analyze/deploy core is cloud-agnostic and works with Tofu or Terraform via the tf alias. Azure coupling is confined to the inspect and telemetry paths. Stage names are hardcoded.
D
9 · Testing & QA
69 tests all pass, but line coverage is 30%. CI runs pytest only; ruff, pylint, bandit and mypy are commented out in pre-commit, and there is no dependency scanning.
C
10 · Technical Debt & Risk
17 risks registered: 2 high, 7 medium, 8 low. Most are cheap to fix. Docs have drifted (references to wrapper.py, examples/ and docs/, none of which exist).

Measured metrics

Runtime LOC
6,055
20 modules · tests 1,352 LOC
Tests
69 / 69
pass in 2.4 s
Line coverage
30%
722 / 2,432 stmts
Pylint
9.72
excl. line-length
Ruff findings
303
186 E501 · 26 auto-fixable
Mypy errors
165
13 files, strict config
Bandit
0 / 0 / 27
high / med / low
Known CVEs (lock)
34
4 transitive packages
Coverage by module
core/security.py
95%
command_constructor.py
74%
inspector.py
56%
deploy.py
54%
services/deployer.py
36%
analyze.py
32%
printer.py
20%
services/graph_runner.py
0%
services/analyzer.py
0%
services/azure.py
0%
main.py
0%
cli.py
0%
core/{config,logging,resource_mgmt}
dead
Ruff findings by rule (top)
E501 line-too-long
186
D415 / D102 / D1xx docs
60
S603 / S607 subprocess
19
UP0xx pyupgrade
20
B007 / B005 / B904 bugbear
4
I001 / C408 misc
4
Known vulnerabilities (pip-audit on uv export)
pyjwt 2.12.1 → ≥2.15.0
18
urllib3 2.6.3 → ≥2.8.0
7
cryptography 46.0.7
7
idna 3.11
2
All four are transitive, mostly pulled in via azure-identity → msal and requests. Some counts include aliases of the same advisory.

Architecture map (as-is)

cli.py (click, 1,095 LOC)→ main.TgWrap (façade)→ services/graph_runner services/analyzer services/deployer services/azure inspector.AzureInspector
services/analyzer → analyze.py (legacy functions) services/deployer → deploy.py (legacy functions) command_constructor → core/security
core/config.py · core/logging.py · core/resource_management.py — never imported sys.exit() in 9 modules (19 calls) click.ClickException raised from services

External processes: terragrunt, tf (tenv alias), git, rsync, az. External HTTP: PyPI on every run, Azure Monitor DCE, ARM, Microsoft Graph. Every command is built as a string and passed through shlex.split, never assembled as an argv list.

Detailed findings per dimension

Click a dimension to expand it. File references use path:line at commit 0ba3c09.

C1 · Architecture & Service Design▶

Justification. The 2025 refactor added a façade (main.TgWrap) and focused services with constructor injection (AnalyzerService receives the graph runner, the command constructor and the Azure service). The boundaries don't fully hold: services terminate the process and depend on the CLI framework, and two generations of code live side by side.

Key findings

  • Clear façade → service decomposition with dependency injection (tgwrap/main.py:37-66).
  • TgCommandConstructor separates CommandConfig, CommandComponents and assembly, which makes it testable (74% coverage).
  • Services call sys.exit: 19 calls across 9 modules, e.g. services/graph_runner.py:91,131, services/deployer.py:358,363,570, services/azure.py:135, services/analyzer.py:286. That makes them impossible to compose or unit-test without patching.
  • Services depend on click: services/azure.py:36-91 and services/graph_runner.py:174,177 raise click.ClickException.
  • Two generations of code coexist: analyze.py and deploy.py (free functions, each creating its own Printer) are wrapped by the services rather than absorbed into them.
  • Orphaned modules: core/config.py (TgWrapConfig), core/logging.py (StructuredLogger) and core/resource_management.py are never imported. Of 10 exception classes in core/exceptions.py, only DeploymentError is used.
  • Constants are duplicated: PLANFILE_NAME, TG_FILE and MINIMUM_TG_VERSION are defined in main.py:30-35, command_constructor.py:115-119 and core/config.py:22-24.

Recommendations

  1. Add an exception hierarchy (reuse core/exceptions.py). Services raise; only cli.py maps exceptions to exit codes and messages.
  2. Delete the orphaned core/* modules, or wire TgWrapConfig in as the single source for constants and env config. Don't leave both.
  3. Move deploy.run_sync and analyze.* into their services (or a domain/ package), passing in the shared Printer.
  4. Pass a Printer or logger protocol into services instead of having them construct one.
C2 · Python Engineering Quality▶

Justification. The basics are in place: uv with a lockfile, Python ≥3.12, yaml.safe_load everywhere, timeouts on every requests call, and no shell=True. But the configured strictness (strict mypy, broad ruff rule set) isn't met, and several latent bugs come from global state and string handling.

Key findings

  • Commands are built as strings, then split: --working-dir {working_dir} is unquoted (command_constructor.py:590, services/graph_runner.py:151), as are the rsync paths (deploy.py:118-121) and git clone {repo} {target_dir} (services/deployer.py:718). Paths containing spaces break, and the repo value goes to git without a -- separator.
  • Shared mutable class state: AzureInspector._role_definitions = {} at class level (inspector.py:46) is mutated in place (inspector.py:155), so all instances share it.
  • Process-global side effects: os.chdir in services/graph_runner.py:299; os.environ mutated for TG_SOURCE and TG_INPUTS_DEBUG (command_constructor.py:153,475,544).
  • Prefix bug: _is_external_dependency uses startswith without a path separator (services/graph_runner.py:475), so /repo/app-shared counts as "inside" /repo/app.
  • Step numbering group + module/100 (services/graph_runner.py:108) collides once a group has 100 or more modules.
  • Type debt: 165 mypy errors (missing annotations on most façade and service methods). 14/20 files fail ruff format --check.
  • Broad except Exception is always annotated, and re-raises in verbose mode, which helps debugging.

Recommendations

  1. Build list[str] argv everywhere (drop shlex.split on composed strings). Add -- before positional git and rsync arguments.
  2. Move mutable class attributes in AzureInspector into __init__. Move the role-definition fetch out of the constructor and make it lazy.
  3. Replace os.chdir with os.walk(root_dir) plus relpath. Pass env to subprocess.run(env=...) instead of mutating os.environ.
  4. Use os.path.commonpath for the containment check. Represent steps as a (group, module) tuple.
  5. Run ruff format and ruff check --fix once, then ratchet mypy per module (start with services/).
B3 · IaC Orchestration Safety (Terraform/Terragrunt operation)▶

Justification. The repo has no .tf or .hcl of its own, which is by design, so this dimension looks at how safely tgwrap drives IaC. The safety-relevant defaults are good and documented in OpenSpec. The main weakness is how module failure is detected.

Key findings

  • --auto-approve is restricted to apply/destroy (command_constructor.py:47,586; main.py:180-181) and backed by openspec/specs/terragrunt-command-flags/spec.md.
  • Defaults are safe: state locking on, -lock=false only for init/plan with a warning (command_constructor.py:518-529), and the minimum Terragrunt version 0.88.0 is enforced.
  • The terrasafe-style deletion guard (TERRASAFE_ALLOW_DELETION, .tf.disabled detection) plus a drift score (major×10 + medium + minor/10) make plan risk quantifiable.
  • destroy walks the graph backwards, and --start-at-step lets an operator resume a partially failed run.
  • A module counts as failed if "error" appears anywhere in stderr (services/graph_runner.py:464). Warnings that mention "error" stop a step-by-step apply, while a non-zero exit with no "error" text is still caught by the return-code check.
  • TG_RO_COMMANDS lists state and force-unlock as read-only, and import appears in both lists (command_constructor.py:16-45). Today this is documentation only, but it will mislead anyone who later gates on these lists.
  • is_deletion_in_disabled_file walks the whole CWD tree once per deletion and builds its regex without re.escape (analyze.py:365-384).

Recommendations

  1. Decide failure from returncode alone. If text heuristics are still needed, match explicit Terragrunt error prefixes.
  2. Fix the command classification and add a test that asserts no write-capable command is in TG_RO_COMMANDS.
  3. Cache the .tf.disabled file scan once per module and escape identifiers.
N/A4 · Kubernetes & Platform Architecture▶

No Kubernetes manifests, Helm charts or operators. That fits a developer and CI CLI. No grade given, and no impact on the overall grade.

C5 · Operational Excellence (CLI / CI lens)▶

Justification. tgwrap mostly runs inside CI pipelines, so what matters operationally is correct exit codes, predictable network behaviour and diagnosable output. Telemetry and resumability are strengths. Silent failure paths and missing retries are not.

Key findings

  • Silent failure: run_sync prints an error and returns normally when rsync is missing or the source path doesn't exist (deploy.py:35-47). deploy then writes version.hcl and the process exits 0.
  • The PyPI version check runs on every run (cli.py:29-62, 5 s timeout). OUTDATED_IGNORE only suppresses the output, not the request, which adds latency and egress in restricted runners.
  • Logging is plain Printer output to stdout/stderr. The structured, secret-masking StructuredLogger exists but is unused, and there are no correlation or run IDs.
  • No retry or backoff on requests (DCE, ARM, Graph). One transient 429 or 5xx loses the telemetry or fails the inspection.
  • Analyze results can be posted to an Azure Monitor DCE, enriched with repo (basic-auth masked), scope and principal (services/azure.py:137-197).
  • Step-by-step mode with resume hints (--start-at-step) and --continue-on-error.

Recommendations

  1. Make run_sync raise DeploymentError, and map it to a non-zero exit in the CLI.
  2. Skip the PyPI check when OUTDATED_IGNORE or CI is set, or cache the result for 24 h.
  3. Use a requests.Session with urllib3.Retry (backoff, respecting Retry-After) for Azure calls.
  4. Either adopt StructuredLogger with a --log-format json option for CI, or delete it.
D9 · Testing & Quality Assurance▶

Justification. The test suite is healthy but narrow: it covers command building, security validation and sync semantics, and leaves the orchestration core untested. The quality tooling is all declared and none of it is enforced.

Key findings

  • 0% coverage for services/graph_runner.py (239 stmts), main.py (218), cli.py (214), services/analyzer.py (129) and services/azure.py (92). These carry the highest-risk logic: group ordering, failure detection, exit codes.
  • CI (.forgejo/workflows/ci.yml) runs only pytest and uv build: no lint, types, bandit, coverage threshold or dependency audit. It also installs uv unpinned via pip install uv.
  • ruff, pylint, bandit, mypy and pydocstyle are commented out in .pre-commit-config.yaml:17-56, while pytest and uv build run on every commit, which is slow and catches the wrong things.
  • Dev tooling is split between [project.optional-dependencies].dev (ruff, pylint, mypy, bandit, …) and [dependency-groups].dev (pytest only), so a plain uv sync leaves out the linters. It also includes an unexplained ruv package.
  • A stale bandit-report.json (from 2025-11) is committed, and the examples/ fixtures that AGENTS.md calls for don't exist.
  • The 69 tests are fast (2.4 s), descriptive, and use monkeypatch instead of real binaries. Security validation is covered at 95%.

Recommendations

  1. Add CI jobs for ruff check, ruff format --check, bandit -ll, pip-audit (or uv audit), and pytest --cov --cov-fail-under=30, ratcheting the threshold up.
  2. Write graph-runner tests with a fake subprocess.run and a small DOT fixture: grouping, include/exclude globs, external-dependency filtering, stop-on-failure exit code.
  3. Add CLI tests via click.testing.CliRunner covering exit-code mapping and the --auto-approve matrix.
  4. Merge the dev dependencies into one [dependency-groups].dev, drop ruv/pbr/safety if unused, and re-enable the pre-commit linters.
B8 · Multi-Cloud & Portability▶
  • The run/analyze/deploy flows are cloud-agnostic. Tofu and Terraform are interchangeable through the tf alias (README "Terraform vs OpenTofu").
  • Azure-specific code is confined to services/azure.py, inspector.py and the template, and is opt-in via --data-collection-endpoint or inspect.
  • STAGES is hardcoded (core/constants.py: global/sbx/dev/qas/run/tst/acc/prd), which couples the tool to one organisation's naming.
  • There is no telemetry abstraction. Adding another sink (OTLP, CloudWatch) would mean editing the analyzer.

Recommendations: make stages configurable (env or config file), and put telemetry behind a small ResultSink protocol with a DCE implementation.

C10 · Technical Debt & Risk▶
  • Documentation has drifted. AGENTS.md:7,14-15,30 and README.md:436-444 reference core/wrapper.py, examples/ and docs/, none of which exist. AI agents following AGENTS.md will be misled.
  • Release hygiene: CHANGELOG.md stops at 1.0.9 while pyproject.toml is at 1.0.10, and there are no git tags.
  • Three remote branches are unmerged (feature/domain-cleaning, feature/follow-rsync-semantics, fix/auto_approve).
  • Duplicated assembly branches in _assemble_final_command (command_constructor.py:603-662) and an unused COMMAND_TEMPLATES dict.
  • OpenSpec change process adopted (two archived changes with proposal, design and spec), and the changelog root-cause notes are high quality.

See the risk register for the prioritised list.

Azure architecture review

CAzure integration posture
AreaObservation
IdentityDefaultAzureCredential supports MI, workload identity and CLI; user-assigned MI is configurable (inspector.py:139)good
Token scopePer-resource scopes (Graph, ARM, monitor.azure.com//.default). Tokens are never printed; the verbose payload omits headersgood
Auth pathsDCE posting shells out to az account get-access-token (services/azure.py:21-94) while inspect uses the SDK credential. Two failure modes, and the DCE path needs the az CLI installedmed
ResilienceRaw requests without retry; 429/Retry-After not honoured; no nextLink / @odata.nextLink pagination in the inspectormed
API lifecycleThe inspector template pins old api-versions: 2014-04-01, 2018-06-01, 2019-09-01, 2021-*low
Error disclosureOn failure, the full az account show JSON (tenant, user) is echoed into the exception (services/azure.py:61-73), and it ends up in CI logslow
Governanceinspect checks naming conventions, existence and RBAC assignments against inspect-config.yml, which is a useful lightweight conformance checkgood
Recommendations (Azure)
  1. Get the DCE token from DefaultAzureCredential().get_token("https://monitor.azure.com//.default") and drop the az subprocess, so there's one auth path that works with workload identity.
  2. Consider azure-monitor-ingestion LogsIngestionClient instead of hand-rolled POSTs. It gives you retries, batching and size limits.
  3. Add a shared Session with Retry(total=5, backoff_factor=1, status_forcelist=[429,500,502,503,504], respect_retry_after_header=True).
  4. Follow nextLink when listing. Bump api-versions to current GA and record them in one table.
  5. When account info is incomplete, report only which fields are missing; don't dump the whole JSON.
No AWS integration exists, so the AWS section is omitted (N/A, not penalised).

Risk register

Likelihood × impact
Impact low
Impact med
Impact high
Likely
R10 R13
R4 R9
R1
Possible
R12 R15 R16
R3 R5 R8 R11
R2
Unlikely
R17
R6 R7 R14

Rows are likelihood, columns are impact on infrastructure change safety or CI reliability.

Distribution
high
2
medium
7
low
8

Effort: S ≤ ½ day, M ≤ 2 days, L > 2 days.

IDSevRiskEvidenceFixEffort
R1highdeploy or sync exits 0 when rsync is missing or the source path doesn't exist, so CI shows green with nothing deployedtgwrap/deploy.py:35-47Raise DeploymentError; map it to exit 1 in the CLI; add a test that asserts the exceptionS
R2high34 known advisories in locked transitive deps (pyjwt 2.12.1, urllib3 2.6.3, cryptography 46.0.7, idna 3.11)uv.lock; pip-audituv lock --upgrade-package pyjwt --upgrade-package urllib3 --upgrade-package cryptography --upgrade-package idna; add an audit job to CIS
R3medargv built from unquoted strings: paths with spaces break; git clone / git switch / rsync lack a -- separator (option injection from manifest values)command_constructor.py:590, services/graph_runner.py:151, deploy.py:118-136, services/deployer.py:692,718Build list[str]; insert --; validate that git_repository doesn't start with -M
R4medFalse-positive failure detection when stderr contains "error"services/graph_runner.py:459-469Decide on return code; keep text only for the skip detectionS
R5medExternal-dependency check uses a string prefix, so sibling dirs with a shared prefix are treated as internalservices/graph_runner.py:471-477os.path.commonpath([a, b]) == aS
R6medClass-level mutable dict shared across AzureInspector instances; network I/O in the constructorinspector.py:46,150-155Initialise in __init__; lazy-load role definitionsS
R7medDead modules suggest capabilities (config, structured logging, timeouts) that don't existcore/config.py, core/logging.py, core/resource_management.pyDelete them, or wire them in deliberatelyS
R8medServices call sys.exit or raise click errors, so they can't be reused or tested in isolation19 sys.exit calls in 9 modulesException hierarchy plus a single exit mapping in cli.pyM
R9medQuality gates not enforced (CI runs pytest only; pre-commit linters disabled); orchestration core at 0% coverage.forgejo/workflows/ci.yml, .pre-commit-config.yaml:17-56Add lint/type/audit/coverage jobs; write graph-runner and CLI testsM–L
R10lowPyPI call on every run, even with OUTDATED_IGNOREcli.py:29-62, cli.py:334Return early when OUTDATED_IGNORE/CI is set; cache the resultS
R11lowProcess-global side effects: os.chdir, os.environ mutation and deletion of TG_SOURCEservices/graph_runner.py:298-327, command_constructor.py:153,475,544Pass cwd= / env= to subprocessS
R12lowStep number collides when a group has 100 or more modulesservices/graph_runner.py:108Use a (group, idx) tuple; parse --start-at-step g.mS
R13lowDocs/agent guidance drift; CHANGELOG lags; no release tags; stale bandit-report.json committedAGENTS.md, README.md:436-444, CHANGELOG.mdFix the references; tag releases; remove the report from gitS
R14lowAzure calls: no retry or pagination; old api-versions; two auth pathsinspector.py:190-214, services/azure.py, templateSee the Azure sectionM
R15lowCommand classification wrong (state and force-unlock marked RO; import listed twice)command_constructor.py:16-45Correct the lists and add an invariant testS
R16lowThe input validator rejects legitimate args ($, {}, \, .. in working dir) even though execution is shell-less, so the restriction buys littlecore/security.py:13-18,59-71Document the threat model; allow -var JSON and relative .. paths (argv lists remove the injection risk)S
R17lowThe .tf.disabled scan walks the whole tree once per deletion; regex not escapedanalyze.py:365-384Scan once, cache, re.escapeS

Trend & historical comparison

Baseline run. ./reports/assessments/ held no earlier reports, so there are no prior grades to compare. This report sets the baseline. The table below is ready for the next run, and a machine-readable score block is embedded in this file (#assessment-data) so future runs can parse it.
Score evolution
Dimension2026-10-18Next runΔ
1 ArchitectureC—baseline
2 Python qualityC—baseline
3 IaC orchestration safetyB—baseline
4 KubernetesN/A——
5 Operational excellenceC—baseline
6 AzureC—baseline
7 AWSN/A——
8 PortabilityB—baseline
9 Testing & QAD—baseline
10 Debt & riskC—baseline
OverallC—baseline
Architecture evolution (from git history, qualitative)
2025-04 → 2025-06
Features added fast: analyze-after-plan, version display, Tofu/Terraform switch via tf.
2025-07-01
"Security enhancements and comprehensive test coverage": core/security.py added, and probably the now-orphaned core/config/logging modules too.
2025-11 → 2025-12
v1.0.0; façade/service refactor (services/*); migration from GitLab to Codeberg/Forgejo CI.
2025-12-28
Unicode path sanitisation (PR #4).
2026-04-10
Poetry → uv migration (OpenSpec-tracked).
2026-06-02 → 06-03
Three successive rsync fixes for containerised CI (ownership, mtime, perms), all done without a regression test harness for run_sync exit behaviour.
2026-10-13
Auto-approve limited to apply/destroy (PR #6, OpenSpec spec).

Narrative: the codebase is moving from a monolithic script to a layered tool, and its change process is maturing (OpenSpec, PRs, detailed changelog). Quality automation hasn't kept pace with the refactor.

Recommendations & roadmap

Now · quick wins (≤ 2 days)

  1. R1: make run_sync raise on missing rsync or source, and add a test.
  2. R2: uv lock --upgrade-package for pyjwt, urllib3, cryptography and idna; add pip-audit to CI.
  3. R4/R5: return-code-based failure detection; commonpath containment.
  4. R10: skip the PyPI check when OUTDATED_IGNORE or CI is set.
  5. CI: add ruff check and ruff format --check (run ruff --fix and format once first). Pin the uv version.
  6. R13: fix the AGENTS.md and README structure sections; delete bandit-report.json; add 1.0.10 to the CHANGELOG; tag releases.

Later · strategic

  1. Fold analyze.py/deploy.py into domain modules, making the service layer the single API (and a possible library entry point).
  2. Pluggable result sinks (DCE, OTLP, JSON file) and a --log-format json option for CI log pipelines.
  3. Configurable stages and naming conventions, so the tool isn't tied to one organisation.
  4. An end-to-end smoke test against a tiny examples/ Terragrunt tree in a container (Tofu with a local backend).
  5. Supply chain: publish with PyPI trusted publishing, attach SBOM and provenance to releases.
Expected grade movement
Today
C
After “Now”
B−
After “Next”
B+
After “Later”
A−

Method, assumptions & limitations

  • Scope: all tracked files at 0ba3c09, excluding .opencode/node_modules (vendored tooling, not part of the product).
  • Tools run: PYTHONPATH=. uv run pytest --cov=tgwrap; ruff check --statistics and ruff format --check (repo config); pylint tgwrap --disable=C0301; mypy tgwrap --ignore-missing-imports (repo strict config); bandit -r tgwrap; pip-audit on uv export (vulnerability DB as of 2026-10-18).
  • Not done: no end-to-end runs against real Terragrunt stacks or Azure subscriptions; behaviour claims rest on static reading plus unit tests.
  • Assumption: tgwrap mostly runs in CI and on developer laptops, so "operational excellence" is judged by exit-code correctness, network behaviour and diagnosability rather than service SLOs.
  • Grading: N/A dimensions (Kubernetes, AWS) don't count towards the overall grade. Dimension 3 was reframed from "Terraform code quality" to "IaC orchestration safety" because the repo has no IaC of its own.
  • Trend: no historical reports, so there are no prior scores to compare. The evolution narrative comes from git log only.