smallsteps 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: "Smallsteps Ratchet Evaluation"
2
+ description: "Evaluates your quality goals"
3
+ inputs:
4
+ SMALLSTEPS_PYTEST_COVERAGE:
5
+ description: "Current live performance value for 'Pytest Coverage'"
6
+ required: true
7
+
8
+ runs:
9
+ using: "composite"
10
+ steps:
11
+ - name: Install uv
12
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
13
+ with:
14
+ enable-cache: true
15
+
16
+ - name: Run Smallsteps Guardrail Validation
17
+ shell: bash
18
+ run: uvx smallsteps-ratchet check
19
+ env:
20
+ SMALLSTEPS_PYTEST_COVERAGE: ${{ inputs.SMALLSTEPS_PYTEST_COVERAGE }}
@@ -0,0 +1,83 @@
1
+ name: CI and Publish
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
17
+ with:
18
+ enable-cache: true
19
+
20
+ - name: Install dependencies
21
+ run: uv sync --all-groups --frozen
22
+
23
+ - name: Lint with ruff
24
+ run: uv run ruff check .
25
+
26
+ - name: Format check with ruff
27
+ run: uv run ruff format --check .
28
+
29
+ - name: Execute Test Suite with JSON Report
30
+ run: uv run pytest --cov --cov-report=json
31
+
32
+ - name: Parse Metrics
33
+ id: extract_metrics
34
+ run: |
35
+ COVERAGE_RAW=$(jq -r '.totals.percent_covered' coverage.json)
36
+ echo "coverage=$COVERAGE_RAW" >> $GITHUB_OUTPUT
37
+
38
+ # - name: Enforce Monotonic Progress Guardrails
39
+ # uses: ./.github/actions/smallsteps
40
+ # with:
41
+ # SMALLSTEPS_PYTEST_COVERAGE: ${{ steps.extract_metrics.outputs.coverage }}
42
+
43
+ publish:
44
+ needs: test
45
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
46
+ runs-on: ubuntu-latest
47
+ environment:
48
+ name: pypi
49
+ url: https://pypi.org/p/nasa-lsp
50
+ permissions:
51
+ id-token: write
52
+ contents: write
53
+
54
+ steps:
55
+ - uses: actions/checkout@v6
56
+ with:
57
+ fetch-depth: 0
58
+ token: ${{ secrets.GITHUB_TOKEN }}
59
+
60
+ - name: Install uv
61
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
62
+ with:
63
+ enable-cache: true
64
+
65
+ - name: Install dependencies
66
+ run: uv sync --all-groups --frozen
67
+
68
+ - name: Configure git
69
+ run: |
70
+ git config user.name "github-actions[bot]"
71
+ git config user.email "github-actions[bot]@users.noreply.github.com"
72
+
73
+ - name: Bump version
74
+ run: uv run bump-my-version bump patch
75
+
76
+ - name: Push version bump
77
+ run: git push --follow-tags
78
+
79
+ - name: Build package
80
+ run: uv build
81
+
82
+ - name: Publish to PyPI
83
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ coverage.json
9
+
10
+ # Virtual environments
11
+ .venv
12
+
13
+
14
+ .coverage
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: smallsteps
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: pydantic>=2.13.4
7
+ Requires-Dist: typer>=0.26.7
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Smallsteps - improve your codebase in small steps
11
+
12
+ Smallsteps allows you to declare ratchets – metrics that must increase over time – and to enforce them via testing or CI pipelines.
13
+
14
+ A typical situation: you want to add test coverage as a health check.
15
+ Initially your coverage is at 42% so you create a CI gate that fails if it falls under 40%. Now you have your baseline secured – so far so good. You make a resolution to yourself that you will increase your coverage to bump up the percentage in your CI action frequently.
16
+ **Somehow that never happens and your coverage stays at 42%.**
17
+
18
+ With smallsteps you declare your goal of 80% coverage and that you want to reach it in 100 days. Smallsteps computes the necessary percentage that must be met for each day (e.g. after 50 days you need 60% coverage) and fails if the goal is not met.
19
+
20
+ ## Example
21
+
22
+ We are using this repository itself as an example. We want to enforce our test coverage to go up.
23
+ So we create a ratchet:
24
+
25
+ ```
26
+ uvx smallsteps add \
27
+ --name="Pytest Coverage" \
28
+ --command="uv run pytest --cov --cov-report=json > /dev/null && jq -r '.totals.percent_covered' coverage.json" \
29
+ --goal=80 \
30
+ --end="2026-10-01"
31
+ ```
32
+
33
+ This created a new file – `smallsteps.toml` – which holds the configuration of our ratchet:
34
+
35
+ ```toml
36
+ # Smallsteps Configuration
37
+
38
+ [[ratchets]]
39
+ name = "Pytest Coverage"
40
+ start = 2026-06-25
41
+ end = 2026-10-01
42
+ initial_value = 57.142857142857146
43
+ goal_value = 80
44
+ command = "uv run pytest --cov --cov-report=json > /dev/null && jq -r '.totals.percent_covered' coverage.json"
45
+
46
+ ```
47
+
48
+ **Note**
49
+
50
+ - You don't need to memorize the command parameters, `smallsteps add` walks you through them interactively.
51
+ - Ratchets can be increasing or decreasing. Percentages (56%) are parsed as floats (.56). So make sure that your goal matches the format of the command output.
52
+
53
+ You can inspect, add or modify the ratchets using the the toml file.
54
+
55
+ ### Checking your Ratchets
56
+
57
+ Run `uvx smallsteps check` to check the status of your ratchets. To simulate the future you can run with `--date`, e.g.:
58
+
59
+ ```
60
+ uv smallsteps check --date 2026-10-31
61
+ Evaluating 1 ratchet(s)...
62
+
63
+ Pytest Coverage: BEHIND (Current: 57.14, Expected Min: 80.00)
64
+
65
+ Progress check failed.
66
+
67
+ ```
68
+
69
+ shows that on the goal date the check will fail if we do not achieve 80% test coverage.
70
+
71
+ ### CI and reading values from env
72
+
73
+ When running `smallsteps check` all commands to gather your metrics are run by smallsteps. This may be not the desired behaviour in CI because you want to run the checks in different workflows / actions and do not want to use smallsteps as a central orchestrator.
74
+
75
+ By default smallsteps looks for environment variables matching the ratchets before running the command. E.g. if `SMALLSTEPS_PYTEST_COVERAGE` is present, the coverage is not re-computed. You can use this in CI to pass outputs from test workflow into the smallsteps action. To get a scaffolding github action with the required input vars for your ratchets run `uvx smallsteps ci`.
76
+
77
+ Have a look at [the action](.github/actions/smallsteps/action.yml) and the whole workflow (not provided by a command because too specific for your setup) to see how the plumbing can work.
78
+
79
+ ## Installation
80
+
81
+ This documentaion assumes you are using `uv`, hence you don't need to do anything despite using the uv tool command (`ux`) to install and run smallsteps.
82
+
83
+ # License
84
+
85
+ MIT
@@ -0,0 +1,76 @@
1
+ # Smallsteps - improve your codebase in small steps
2
+
3
+ Smallsteps allows you to declare ratchets – metrics that must increase over time – and to enforce them via testing or CI pipelines.
4
+
5
+ A typical situation: you want to add test coverage as a health check.
6
+ Initially your coverage is at 42% so you create a CI gate that fails if it falls under 40%. Now you have your baseline secured – so far so good. You make a resolution to yourself that you will increase your coverage to bump up the percentage in your CI action frequently.
7
+ **Somehow that never happens and your coverage stays at 42%.**
8
+
9
+ With smallsteps you declare your goal of 80% coverage and that you want to reach it in 100 days. Smallsteps computes the necessary percentage that must be met for each day (e.g. after 50 days you need 60% coverage) and fails if the goal is not met.
10
+
11
+ ## Example
12
+
13
+ We are using this repository itself as an example. We want to enforce our test coverage to go up.
14
+ So we create a ratchet:
15
+
16
+ ```
17
+ uvx smallsteps add \
18
+ --name="Pytest Coverage" \
19
+ --command="uv run pytest --cov --cov-report=json > /dev/null && jq -r '.totals.percent_covered' coverage.json" \
20
+ --goal=80 \
21
+ --end="2026-10-01"
22
+ ```
23
+
24
+ This created a new file – `smallsteps.toml` – which holds the configuration of our ratchet:
25
+
26
+ ```toml
27
+ # Smallsteps Configuration
28
+
29
+ [[ratchets]]
30
+ name = "Pytest Coverage"
31
+ start = 2026-06-25
32
+ end = 2026-10-01
33
+ initial_value = 57.142857142857146
34
+ goal_value = 80
35
+ command = "uv run pytest --cov --cov-report=json > /dev/null && jq -r '.totals.percent_covered' coverage.json"
36
+
37
+ ```
38
+
39
+ **Note**
40
+
41
+ - You don't need to memorize the command parameters, `smallsteps add` walks you through them interactively.
42
+ - Ratchets can be increasing or decreasing. Percentages (56%) are parsed as floats (.56). So make sure that your goal matches the format of the command output.
43
+
44
+ You can inspect, add or modify the ratchets using the the toml file.
45
+
46
+ ### Checking your Ratchets
47
+
48
+ Run `uvx smallsteps check` to check the status of your ratchets. To simulate the future you can run with `--date`, e.g.:
49
+
50
+ ```
51
+ uv smallsteps check --date 2026-10-31
52
+ Evaluating 1 ratchet(s)...
53
+
54
+ Pytest Coverage: BEHIND (Current: 57.14, Expected Min: 80.00)
55
+
56
+ Progress check failed.
57
+
58
+ ```
59
+
60
+ shows that on the goal date the check will fail if we do not achieve 80% test coverage.
61
+
62
+ ### CI and reading values from env
63
+
64
+ When running `smallsteps check` all commands to gather your metrics are run by smallsteps. This may be not the desired behaviour in CI because you want to run the checks in different workflows / actions and do not want to use smallsteps as a central orchestrator.
65
+
66
+ By default smallsteps looks for environment variables matching the ratchets before running the command. E.g. if `SMALLSTEPS_PYTEST_COVERAGE` is present, the coverage is not re-computed. You can use this in CI to pass outputs from test workflow into the smallsteps action. To get a scaffolding github action with the required input vars for your ratchets run `uvx smallsteps ci`.
67
+
68
+ Have a look at [the action](.github/actions/smallsteps/action.yml) and the whole workflow (not provided by a command because too specific for your setup) to see how the plumbing can work.
69
+
70
+ ## Installation
71
+
72
+ This documentaion assumes you are using `uv`, hence you don't need to do anything despite using the uv tool command (`ux`) to install and run smallsteps.
73
+
74
+ # License
75
+
76
+ MIT
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "smallsteps"
3
+ version = "0.1.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "pydantic>=2.13.4",
9
+ "typer>=0.26.7",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "bump-my-version>=1.4.1",
15
+ "ruff>=0.15.19",
16
+ ]
17
+ test = [
18
+ "pytest>=9.0.3",
19
+ "pytest-cov>=7.1.0",
20
+ ]
21
+
22
+ [tool.uv]
23
+ package = true
24
+
25
+ [project.scripts]
26
+ smallsteps = "smallsteps.cli:app"
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.bumpversion]
33
+ current_version = "0.1.1"
34
+ commit = true
35
+ tag = true
36
+ tag_name = "v{new_version}"
37
+ message = "Bump version: {current_version} → {new_version} [skip ci]"
38
+
39
+ [[tool.bumpversion.files]]
40
+ filename = "pyproject.toml"
41
+ search = 'version = "{current_version}"'
42
+ replace = 'version = "{new_version}"'
@@ -0,0 +1,9 @@
1
+ # Smallsteps Configuration
2
+
3
+ [[ratchets]]
4
+ name = "Pytest Coverage"
5
+ start = 2026-06-25
6
+ end = 2026-10-01
7
+ initial_value = 57.142857142857146
8
+ goal_value = 80
9
+ command = "uv run pytest --cov --cov-report=json > /dev/null && jq -r '.totals.percent_covered' coverage.json"
@@ -0,0 +1,12 @@
1
+ from smallsteps.cli import app
2
+ from smallsteps.command_runner import CommandRunner
3
+ from smallsteps.prober import Prober
4
+ from smallsteps.ratchet import Ratchet, evaluate
5
+
6
+ __all__ = [
7
+ "app",
8
+ "Ratchet",
9
+ "evaluate",
10
+ "CommandRunner",
11
+ "Prober",
12
+ ]
@@ -0,0 +1,69 @@
1
+ import re
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from smallsteps.ratchet import Ratchet
6
+
7
+
8
+ def ratchet_to_slug(ratchet: Ratchet) -> str:
9
+ """Generates a standardized environment variable key for a given ratchet name."""
10
+ return f"SMALLSTEPS_{re.sub(r'[^a-zA-Z0-9]', '_', ratchet.name).upper()}"
11
+
12
+
13
+ class GitHubCIAdapter:
14
+ """Generates a custom local GitHub Composite Action matched to the project metrics."""
15
+
16
+ def generate_action(self, ratchets: List[Ratchet]) -> str:
17
+ """Generates an action.yml manifest string with dynamic inputs, setup stubs, and mapping."""
18
+ yaml_lines = [
19
+ "name: 'Smallsteps Ratchet Evaluation'",
20
+ "description: 'Evaluates your quality goals'",
21
+ ]
22
+
23
+ # 1. Append inputs only if there are active ratchets to list
24
+ if ratchets:
25
+ yaml_lines.append("inputs:")
26
+ for r in ratchets:
27
+ slug = ratchet_to_slug(r)
28
+ yaml_lines.extend(
29
+ [
30
+ f" {slug}:",
31
+ f" description: \"Current live performance value for '{r.name}'\"",
32
+ " required: true",
33
+ ]
34
+ )
35
+
36
+ # 2. Append core composite block infrastructure and the installation placeholder
37
+ yaml_lines.extend(
38
+ [
39
+ "",
40
+ "runs:",
41
+ " using: 'composite'",
42
+ " steps:",
43
+ " - name: Install uv",
44
+ " uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0",
45
+ " with:",
46
+ " enable-cache: true",
47
+ "",
48
+ " - name: Run Smallsteps Guardrail Validation",
49
+ " shell: bash",
50
+ " run: uvx smallsteps-ratchet check",
51
+ ]
52
+ )
53
+
54
+ # 3. Append step context environment maps sequentially via loop execution
55
+ if ratchets:
56
+ yaml_lines.append(" env:")
57
+ for r in ratchets:
58
+ slug = ratchet_to_slug(r)
59
+ yaml_lines.append(f" {slug}: ${{{{ inputs.{slug} }}}}")
60
+
61
+ return "\n".join(yaml_lines) + "\n"
62
+
63
+ def save(self, path: Path, content: str) -> None:
64
+ """Writes the custom composite action metadata safely out to file."""
65
+ try:
66
+ path.parent.mkdir(parents=True, exist_ok=True)
67
+ path.write_text(content, encoding="utf-8")
68
+ except Exception as e:
69
+ raise IOError(f"Failed to write local custom action configuration: {e}")
@@ -0,0 +1,275 @@
1
+ from datetime import date, datetime, timedelta
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from smallsteps.ci_adapter import GitHubCIAdapter
7
+ from smallsteps.cli_helpers import find_git_root
8
+ from smallsteps.command_runner import (
9
+ CommandRunnerWithEnvLookUp,
10
+ OSCommandRunner,
11
+ OSEnvAdapter,
12
+ )
13
+ from smallsteps.config_adapter import TOMLConfigAdapter
14
+ from smallsteps.parsing import parse_numeric_input
15
+ from smallsteps.prober import Prober, StaticDateProvider, local_system_date_provider
16
+ from smallsteps.ratchet import (
17
+ Ratchet,
18
+ RatchetEvaluationFailure,
19
+ RatchetEvaluationSuccess,
20
+ )
21
+
22
+ app = typer.Typer(
23
+ help="Smallsteps: Monotonic progress tracking and enforcement.",
24
+ no_args_is_help=True,
25
+ )
26
+
27
+ DEFAULT_CONFIG_FILE = Path("smallsteps.toml")
28
+
29
+
30
+ def resolve_config_path(explicit_path: Path | None) -> Path:
31
+ if explicit_path is not None:
32
+ return explicit_path
33
+ return find_git_root() / DEFAULT_CONFIG_FILE
34
+
35
+
36
+ @app.command(name="add")
37
+ def add(
38
+ name: str = typer.Option(
39
+ None, "--name", "-n", help="Unique name for your ratchet."
40
+ ),
41
+ goal_val: str = typer.Option(
42
+ None, "--goal", "-g", help="Goal value (e.g. .9 or 250)."
43
+ ),
44
+ command_str: str = typer.Option(
45
+ None, "--command", "-cmd", help="Shell command to compute the value."
46
+ ),
47
+ end_date: datetime | None = typer.Option(
48
+ None, "--end", "-e", help="Ratchet goal end date (YYYY-MM-DD)."
49
+ ),
50
+ config: Path | None = typer.Option(
51
+ None, "--config", "-c", help="Custom path to config file."
52
+ ),
53
+ ):
54
+ """Adds a ratchet to your configuration (Auto-creates smallsteps.toml if missing)."""
55
+ config_path = resolve_config_path(config)
56
+ config_adapter = TOMLConfigAdapter()
57
+
58
+ # 1. Collect or prompt for the name first
59
+ final_name = name or typer.prompt("Ratchet name (e.g., Test Coverage)")
60
+
61
+ existing_ratchets = config_adapter.load(config_path)
62
+ if any(r.name == final_name for r in existing_ratchets):
63
+ typer.secho(
64
+ f"❌ A ratchet named '{final_name}' already exists.",
65
+ fg=typer.colors.RED,
66
+ err=True,
67
+ )
68
+ raise typer.Exit(code=1)
69
+
70
+ # 2. Ask for command and use it for baseline
71
+ final_cmd = command_str or typer.prompt(
72
+ "Shell command to compute the ratchet value"
73
+ )
74
+ typer.echo("Running command to establish live baseline value...")
75
+ runner = OSCommandRunner()
76
+ try:
77
+ # Create a temporary Ratchet instance to satisfy our runner protocol
78
+ temp_ratchet = Ratchet(
79
+ name=final_name,
80
+ start=date.today(),
81
+ end=date.today(),
82
+ initial_value=0,
83
+ goal_value=100,
84
+ command=final_cmd,
85
+ )
86
+ discovered_baseline = runner(temp_ratchet)
87
+
88
+ except Exception as e:
89
+ typer.secho(
90
+ f"❌ Failed to calculate baseline using that command.\nError: {e}",
91
+ fg=typer.colors.RED,
92
+ err=True,
93
+ )
94
+ raise typer.Exit(code=1)
95
+
96
+ typer.echo(f"Live baseline discovered: {discovered_baseline:.2f}")
97
+ # 3. Ask for the Goal
98
+ raw_goal = goal_val or typer.prompt(
99
+ f"What is your goal value? (Current: {discovered_baseline})"
100
+ )
101
+
102
+ try:
103
+ final_goal = parse_numeric_input(raw_goal)
104
+ except ValueError as e:
105
+ typer.secho(f"❌ Invalid goal value: {e}", fg=typer.colors.RED, err=True)
106
+ raise typer.Exit(code=1)
107
+
108
+ # 4. Handle Target Goal Date
109
+ if end_date:
110
+ final_end_date = end_date.date()
111
+ else:
112
+ goal_date_str = typer.prompt(
113
+ f"What is the end date by which the goal must be met? (YYYY-MM-DD, e.g. in 30 days its {(date.today() + timedelta(days=30)).isoformat()} ) ",
114
+ )
115
+ try:
116
+ final_end_date = date.fromisoformat(goal_date_str)
117
+ except ValueError:
118
+ typer.secho(
119
+ f"❌ Invalid date format '{goal_date_str}'. Please use YYYY-MM-DD.",
120
+ fg=typer.colors.RED,
121
+ err=True,
122
+ )
123
+ raise typer.Exit(code=1)
124
+
125
+ # 5. Construct Ratchet
126
+ try:
127
+ validated_ratchet = Ratchet(
128
+ name=final_name,
129
+ start=date.today(),
130
+ end=final_end_date,
131
+ initial_value=discovered_baseline,
132
+ goal_value=final_goal,
133
+ command=final_cmd,
134
+ )
135
+ except Exception as e:
136
+ typer.secho(
137
+ f"❌ Ratchet validation failed:\n{e}", fg=typer.colors.RED, err=True
138
+ )
139
+ raise typer.Exit(code=1)
140
+
141
+ # 6. Write
142
+ config_adapter.append(config_path, validated_ratchet)
143
+ typer.secho(
144
+ f"✨ Successfully added '{validated_ratchet.name}' to {config_path.name}!",
145
+ fg=typer.colors.GREEN,
146
+ )
147
+
148
+
149
+ @app.command(name="check")
150
+ def check(
151
+ date: datetime | None = typer.Option(
152
+ None,
153
+ "--date",
154
+ "-d",
155
+ help="Timetravel to a specific date to check your ratchets at.",
156
+ ),
157
+ config: Path | None = typer.Option(None, "--config", "-c"),
158
+ ):
159
+ """Evaluates all ratchets and enforces progress."""
160
+ config_path = resolve_config_path(config)
161
+ config_adapter = TOMLConfigAdapter()
162
+
163
+ try:
164
+ ratchets = config_adapter.load(config_path)
165
+ except RuntimeError as e:
166
+ typer.secho(str(e), fg=typer.colors.RED, err=True)
167
+ raise typer.Exit(code=1)
168
+
169
+ if not ratchets:
170
+ typer.secho(
171
+ f"No active ratchets found in {config_path.name}.",
172
+ fg=typer.colors.YELLOW,
173
+ )
174
+ return
175
+
176
+ # Wire up runner & prober pipeline
177
+ runner_with_lookup = CommandRunnerWithEnvLookUp(
178
+ env_adapter=OSEnvAdapter(), command_runner=OSCommandRunner()
179
+ )
180
+ date_provider = (
181
+ StaticDateProvider(date.date()) if date else local_system_date_provider
182
+ )
183
+ prober = Prober(command_runner=runner_with_lookup, date_provider=date_provider)
184
+
185
+ any_failed = False
186
+ typer.echo(f"Evaluating {len(ratchets)} ratchet(s)...\n")
187
+
188
+ for ratchet in ratchets:
189
+ eval_result = prober.probe(ratchet)
190
+
191
+ match eval_result:
192
+ # TRACK 1: Metric is completely healthy and on track
193
+ case RatchetEvaluationSuccess(is_healthy=True) as success:
194
+ typer.secho(
195
+ f"{ratchet.name}: ON TRACK "
196
+ f"(Current: {success.current_value:.2f}, Expected Min: {success.expected_value:.2f})",
197
+ fg=typer.colors.GREEN,
198
+ )
199
+
200
+ # TRACK 2: Command executed fine, but progress dropped below expected target bounds
201
+ case RatchetEvaluationSuccess(is_healthy=False) as below:
202
+ any_failed = True
203
+ typer.secho(
204
+ f"{ratchet.name}: BEHIND "
205
+ f"(Current: {below.current_value:.2f}, Expected Min: {below.expected_value:.2f})",
206
+ fg=typer.colors.RED,
207
+ )
208
+
209
+ # TRACK 3: Underlying shell execution exploded completely (command error, timeout, missing binary)
210
+ case RatchetEvaluationFailure() as failure:
211
+ any_failed = True
212
+
213
+ typer.secho(
214
+ f"{ratchet.name} EVALUATION FAILED! \n"
215
+ f"Details: {failure.error_message}",
216
+ fg=typer.colors.RED,
217
+ err=True,
218
+ )
219
+
220
+ if any_failed:
221
+ typer.echo("\nProgress check failed.")
222
+ raise typer.Exit(code=1)
223
+
224
+ typer.echo("All monitored project metrics are completely healthy.")
225
+
226
+
227
+ @app.command(name="ci")
228
+ def ci(
229
+ overwrite: bool = typer.Option(
230
+ False,
231
+ "--overwrite",
232
+ "-o",
233
+ help="Force overwrite the action.yml file if it already exists.",
234
+ ),
235
+ config: Path | None = typer.Option(
236
+ None, "--config", "-c", help="Custom path to the smallsteps configuration file."
237
+ ),
238
+ ):
239
+ """Generates a scaffoling for a github action"""
240
+ config_path = resolve_config_path(config)
241
+ config_adapter = TOMLConfigAdapter()
242
+ ci_adapter = GitHubCIAdapter()
243
+
244
+ # 1. Check if the target file destination already exists
245
+ action_path = Path(".github/actions/smallsteps/action.yml")
246
+ if action_path.exists() and not overwrite:
247
+ typer.secho(
248
+ f"Action manifest already exists at:\n {action_path.resolve()}\n\n"
249
+ f"To overwrite this file, re-run this command with the overwrite flag:\n"
250
+ f"uv run smallsteps ci --overwrite",
251
+ fg=typer.colors.RED,
252
+ err=True,
253
+ )
254
+ raise typer.Exit(code=1)
255
+
256
+ # 2. Extract configuration records
257
+ try:
258
+ ratchets = config_adapter.load(config_path)
259
+ except RuntimeError as e:
260
+ typer.secho(str(e), fg=typer.colors.RED, err=True)
261
+ raise typer.Exit(code=1)
262
+
263
+ # 3. Process layout formatting maps
264
+ yaml_content = ci_adapter.generate_action(ratchets)
265
+
266
+ # 4. Save out structural changes
267
+ try:
268
+ ci_adapter.save(action_path, yaml_content)
269
+ typer.secho(
270
+ f"🚀 Custom GitHub Composite Action successfully written to:\n {action_path}",
271
+ fg=typer.colors.GREEN,
272
+ )
273
+ except Exception as e:
274
+ typer.secho(str(e), fg=typer.colors.RED, err=True)
275
+ raise typer.Exit(code=1)