opentia 1.0.0__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.
opentia-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 byterey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
opentia-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: opentia
3
+ Version: 1.0.0
4
+ Summary: Test Impact Analysis — selects only tests affected by a git diff
5
+ Author-email: byterey <reymund.lapera@gmail.com>
6
+ License: MIT
7
+ Keywords: testing,test-impact-analysis,dotnet,csharp,java,nodejs,tia
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # Test Impact Analysis — C# / .NET
25
+
26
+ Analyses a git diff and selects only the tests whose execution path could have been affected by the change. Skips the full suite on every push.
27
+
28
+ **No external dependencies** — Python 3.8+ stdlib only.
29
+
30
+ ---
31
+
32
+ ## Requirements
33
+
34
+ | Tool | Version | Purpose |
35
+ |---|---|---|
36
+ | Python | 3.8+ | Run the script |
37
+ | Git | any | Diff source |
38
+ | .NET SDK | 8.0+ | Build and run `sample-app` tests |
39
+
40
+ ---
41
+
42
+ ## Quick start
43
+
44
+ ```bash
45
+ # Analyse the last commit against the one before it
46
+ python assess_impact.py --base HEAD~1 --root <path-to-your-solution>
47
+
48
+ # Analyse uncommitted (staged + unstaged) changes — no commit needed
49
+ python assess_impact.py --unstaged --root <path-to-your-solution>
50
+
51
+ # Analyse and immediately run the selected tests
52
+ python assess_impact.py --base HEAD~1 --root <path-to-your-solution> --run
53
+ ```
54
+
55
+ `--root` is where the `.sln` / `.csproj` files live. It does not need to be the git root — the script locates the actual git root automatically via `git rev-parse --show-toplevel`.
56
+
57
+ ---
58
+
59
+ ## Validating the script with sample-app
60
+
61
+ `sample-app/` is a self-contained C# solution purpose-built to verify the script against realistic dependency scenarios.
62
+
63
+ ### Dependency graph
64
+
65
+ ```
66
+ SampleApp.Core (no deps) ← 92 tests in SampleApp.Core.Tests
67
+
68
+ SampleApp.Services (depends on Core) ← 60 tests in SampleApp.Services.Tests
69
+
70
+ SampleApp.Api (depends on Services) [no test project]
71
+ ```
72
+
73
+ **Key implication:** changing anything in `Core` triggers **both** test projects because `Services` depends on `Core` — the BFS propagates transitively.
74
+
75
+ ### Step 1 — Confirm baseline
76
+
77
+ ```bash
78
+ cd sample-app
79
+ dotnet test SampleApp.sln
80
+ # Expected: 152 passed, 0 failed
81
+ cd ..
82
+ ```
83
+
84
+ ### Step 2 — Run validation scenarios
85
+
86
+ Each scenario tests a distinct behaviour. Use `--unstaged` to avoid committing:
87
+
88
+ **Scenario 1 — Ignored file → no tests selected**
89
+
90
+ ```bash
91
+ echo "# change" >> sample-app/.gitignore
92
+ python assess_impact.py --unstaged --root sample-app
93
+ # Expected: no tests to run (file type is ignored)
94
+ git checkout sample-app/.gitignore
95
+ ```
96
+
97
+ **Scenario 2 — Services-only change → only `Services.Tests`**
98
+
99
+ ```bash
100
+ echo "// change" >> sample-app/src/SampleApp.Services/PricingService.cs
101
+ python assess_impact.py --unstaged --root sample-app
102
+ # Expected: SampleApp.Services.Tests only, filtered to PricingServiceTests
103
+ git checkout sample-app/src/SampleApp.Services/PricingService.cs
104
+ ```
105
+
106
+ **Scenario 3 — Core change → both test projects (transitive)**
107
+
108
+ ```bash
109
+ echo "// change" >> sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
110
+ python assess_impact.py --unstaged --root sample-app
111
+ # Expected: SampleApp.Core.Tests AND SampleApp.Services.Tests
112
+ # Services.Tests is included because Services depends on Core (BFS)
113
+ git checkout sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
114
+ ```
115
+
116
+ **Scenario 4 — Infrastructure change → all tests forced**
117
+
118
+ ```bash
119
+ echo " " >> sample-app/SampleApp.sln
120
+ python assess_impact.py --unstaged --root sample-app
121
+ # Expected: run_all = true, all test projects
122
+ git checkout sample-app/SampleApp.sln
123
+ ```
124
+
125
+ ### Step 3 — Read the output
126
+
127
+ ```
128
+ ──────────────────────────────────────────────────────────────────
129
+ TEST IMPACT ANALYSIS
130
+ ──────────────────────────────────────────────────────────────────
131
+ Status : Targeted run ← "RUN ALL TESTS" means a fallback was triggered
132
+ Affected test projects (1):
133
+ • SampleApp.Services.Tests
134
+ Affected test classes (1):
135
+ • PricingServiceTests
136
+ dotnet command:
137
+ dotnet test "...SampleApp.Services.Tests.csproj" --filter "FullyQualifiedName~PricingServiceTests"
138
+ ──────────────────────────────────────────────────────────────────
139
+ ```
140
+
141
+ `Status: Targeted run` means the script selected a subset. `Status: RUN ALL TESTS` means it fell back to running everything (expected for Scenario 4).
142
+
143
+ ### Additional scenarios
144
+
145
+ | File to change | Expected result |
146
+ |---|---|
147
+ | `src/SampleApp.Core/Models/Product.cs` | Both test projects, filtered to `ProductTests` + dependent service tests |
148
+ | `tests/SampleApp.Core.Tests/Utilities/StringHelperTests.cs` | `Core.Tests` only, filtered to `StringHelperTests` |
149
+ | `src/SampleApp.Services/appsettings.json` | `Services.Tests`, no class filter (config file) |
150
+ | `src/SampleApp.Services/SampleApp.Services.csproj` | All tests, `run_all = true` (infra file) |
151
+
152
+ ---
153
+
154
+ ## Usage reference
155
+
156
+ ```
157
+ python assess_impact.py [OPTIONS] [-- DOTNET_ARGS]
158
+
159
+ --base REF Git ref to diff against (e.g. HEAD~1, main, origin/main)
160
+ --head REF Head ref to diff from (default: HEAD)
161
+ --root DIR Directory containing .sln / .csproj files (default: .)
162
+ --strategy project | convention | symbol | hybrid (default: hybrid)
163
+ --output, -o human | json | github-actions | azure-devops (default: human)
164
+ --run Execute dotnet test after analysis
165
+ --unstaged Analyse working-tree changes instead of a git diff
166
+ -- Everything after this is forwarded to dotnet test
167
+ ```
168
+
169
+ ### Strategies
170
+
171
+ | Strategy | What it does |
172
+ |---|---|
173
+ | `project` | Parse `.sln`/`.csproj` to find which test projects reference the changed source project (BFS-transitive through the dependency graph) |
174
+ | `convention` | `FooService.cs` → look for `FooServiceTests.cs`, `FooServiceTest.cs`, `TestFooService.cs` |
175
+ | `symbol` | Extract `public class/interface/enum` names from the changed file; grep all test `.cs` files for references |
176
+ | `hybrid` | All three combined (default) |
177
+
178
+ ### Output formats
179
+
180
+ ```bash
181
+ # Human-readable (default)
182
+ python assess_impact.py --base HEAD~1 --root sample-app
183
+
184
+ # JSON — pipe into scripts or CI steps
185
+ python assess_impact.py --base HEAD~1 --root sample-app --output json
186
+
187
+ # GitHub Actions — prints `echo "key=value" >> $GITHUB_OUTPUT` lines
188
+ python assess_impact.py --base HEAD~1 --root sample-app --output github-actions
189
+
190
+ # Azure DevOps — prints `##vso[task.setvariable ...]` lines
191
+ python assess_impact.py --base HEAD~1 --root sample-app --output azure-devops
192
+ ```
193
+
194
+ ### JSON output fields
195
+
196
+ ```jsonc
197
+ {
198
+ "run_all": false, // true = targeted selection was abandoned
199
+ "test_filter": "FullyQualifiedName~PricingServiceTests",
200
+ "test_project_paths": ["...SampleApp.Services.Tests.csproj"],
201
+ "affected_test_projects": ["SampleApp.Services.Tests"],
202
+ "affected_test_classes": ["PricingServiceTests"],
203
+ "dotnet_command": "dotnet test \"...\" --filter \"...\"",
204
+ "reason": "Analysis complete",
205
+ "strategy_notes": [] // warnings / fallback explanations
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## CI integration
212
+
213
+ ### GitHub Actions
214
+
215
+ ```yaml
216
+ - name: Test Impact Analysis
217
+ id: tia
218
+ run: python assess_impact.py --base ${{ github.event.before }} --root sample-app --output github-actions
219
+
220
+ - name: Run affected tests
221
+ if: steps.tia.outputs.has_tests == 'true'
222
+ run: ${{ steps.tia.outputs.dotnet_command }}
223
+ ```
224
+
225
+ Available outputs: `test_filter`, `run_all`, `has_tests`, `test_project_paths`, `dotnet_command`.
226
+
227
+ ### Azure DevOps
228
+
229
+ ```yaml
230
+ - script: python assess_impact.py --base $(System.PullRequest.TargetBranch) --root sample-app --output azure-devops
231
+ displayName: Test Impact Analysis
232
+
233
+ - script: $(dotnetCommand)
234
+ condition: eq(variables['hasTests'], 'true')
235
+ displayName: Run affected tests
236
+ ```
237
+
238
+ Available variables: `testFilter`, `runAllTests`, `hasTests`, `testProjectPaths`, `dotnetCommand`.
239
+
240
+ ---
241
+
242
+ ## How it works
243
+
244
+ ```
245
+ git diff --name-status base..head
246
+
247
+ classify each file → INFRA | IGNORED | CS_SOURCE | CONFIG | UNKNOWN
248
+
249
+ INFRA → run all tests
250
+ IGNORED→ skip
251
+ UNKNOWN→ run all tests (safe fallback)
252
+ CS_SOURCE / CONFIG → run 3 strategies ↓
253
+
254
+ discover_projects() parse .sln + glob .csproj (excludes obj/ bin/)
255
+ build_reverse_deps() BFS: {source_project → set of test projects that cover it}
256
+ ↓ per CS_SOURCE file:
257
+ strategy 1: project dependency graph (ownership → reverse dep lookup)
258
+ strategy 2: convention mapping (FooService → FooServiceTests)
259
+ strategy 3: symbol search (public types → grep cached test files)
260
+ ↓ per CONFIG file:
261
+ find owning project → run its test projects (no class filter)
262
+
263
+ build_filter() FullyQualifiedName~A|FullyQualifiedName~B
264
+ capped at 40 classes; drops to project-level if exceeded
265
+
266
+ ImpactResult → formatter → stdout
267
+ ```
268
+
269
+ **Fallback escalation** — rather than silently skipping tests, the script escalates:
270
+ 1. `.cs` file not owned by any known project → run all test projects
271
+ 2. Source project changed but dependency graph finds no covering tests → run all test projects
272
+ 3. Config file not owned by any project → run all test projects
273
+
274
+ ---
275
+
276
+ ## Extending to other languages
277
+
278
+ To add Python, Node.js, or Java support, provide:
279
+
280
+ 1. A `discover_projects()` equivalent that returns `List[Project]` for that ecosystem
281
+ 2. Three strategy functions matching the signatures of `strategy_dependency_graph`, `strategy_convention`, `strategy_symbol_search`
282
+ 3. New entries in `INFRA_EXTENSIONS`, `IGNORED_EXTENSIONS`, and `CONFIG_EXTENSIONS` for that ecosystem's file types
283
+
284
+ The git analysis, file classification pipeline, output formatters, and CI integration are language-agnostic and require no changes.
@@ -0,0 +1,261 @@
1
+ # Test Impact Analysis — C# / .NET
2
+
3
+ Analyses a git diff and selects only the tests whose execution path could have been affected by the change. Skips the full suite on every push.
4
+
5
+ **No external dependencies** — Python 3.8+ stdlib only.
6
+
7
+ ---
8
+
9
+ ## Requirements
10
+
11
+ | Tool | Version | Purpose |
12
+ |---|---|---|
13
+ | Python | 3.8+ | Run the script |
14
+ | Git | any | Diff source |
15
+ | .NET SDK | 8.0+ | Build and run `sample-app` tests |
16
+
17
+ ---
18
+
19
+ ## Quick start
20
+
21
+ ```bash
22
+ # Analyse the last commit against the one before it
23
+ python assess_impact.py --base HEAD~1 --root <path-to-your-solution>
24
+
25
+ # Analyse uncommitted (staged + unstaged) changes — no commit needed
26
+ python assess_impact.py --unstaged --root <path-to-your-solution>
27
+
28
+ # Analyse and immediately run the selected tests
29
+ python assess_impact.py --base HEAD~1 --root <path-to-your-solution> --run
30
+ ```
31
+
32
+ `--root` is where the `.sln` / `.csproj` files live. It does not need to be the git root — the script locates the actual git root automatically via `git rev-parse --show-toplevel`.
33
+
34
+ ---
35
+
36
+ ## Validating the script with sample-app
37
+
38
+ `sample-app/` is a self-contained C# solution purpose-built to verify the script against realistic dependency scenarios.
39
+
40
+ ### Dependency graph
41
+
42
+ ```
43
+ SampleApp.Core (no deps) ← 92 tests in SampleApp.Core.Tests
44
+
45
+ SampleApp.Services (depends on Core) ← 60 tests in SampleApp.Services.Tests
46
+
47
+ SampleApp.Api (depends on Services) [no test project]
48
+ ```
49
+
50
+ **Key implication:** changing anything in `Core` triggers **both** test projects because `Services` depends on `Core` — the BFS propagates transitively.
51
+
52
+ ### Step 1 — Confirm baseline
53
+
54
+ ```bash
55
+ cd sample-app
56
+ dotnet test SampleApp.sln
57
+ # Expected: 152 passed, 0 failed
58
+ cd ..
59
+ ```
60
+
61
+ ### Step 2 — Run validation scenarios
62
+
63
+ Each scenario tests a distinct behaviour. Use `--unstaged` to avoid committing:
64
+
65
+ **Scenario 1 — Ignored file → no tests selected**
66
+
67
+ ```bash
68
+ echo "# change" >> sample-app/.gitignore
69
+ python assess_impact.py --unstaged --root sample-app
70
+ # Expected: no tests to run (file type is ignored)
71
+ git checkout sample-app/.gitignore
72
+ ```
73
+
74
+ **Scenario 2 — Services-only change → only `Services.Tests`**
75
+
76
+ ```bash
77
+ echo "// change" >> sample-app/src/SampleApp.Services/PricingService.cs
78
+ python assess_impact.py --unstaged --root sample-app
79
+ # Expected: SampleApp.Services.Tests only, filtered to PricingServiceTests
80
+ git checkout sample-app/src/SampleApp.Services/PricingService.cs
81
+ ```
82
+
83
+ **Scenario 3 — Core change → both test projects (transitive)**
84
+
85
+ ```bash
86
+ echo "// change" >> sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
87
+ python assess_impact.py --unstaged --root sample-app
88
+ # Expected: SampleApp.Core.Tests AND SampleApp.Services.Tests
89
+ # Services.Tests is included because Services depends on Core (BFS)
90
+ git checkout sample-app/src/SampleApp.Core/Utilities/MathHelper.cs
91
+ ```
92
+
93
+ **Scenario 4 — Infrastructure change → all tests forced**
94
+
95
+ ```bash
96
+ echo " " >> sample-app/SampleApp.sln
97
+ python assess_impact.py --unstaged --root sample-app
98
+ # Expected: run_all = true, all test projects
99
+ git checkout sample-app/SampleApp.sln
100
+ ```
101
+
102
+ ### Step 3 — Read the output
103
+
104
+ ```
105
+ ──────────────────────────────────────────────────────────────────
106
+ TEST IMPACT ANALYSIS
107
+ ──────────────────────────────────────────────────────────────────
108
+ Status : Targeted run ← "RUN ALL TESTS" means a fallback was triggered
109
+ Affected test projects (1):
110
+ • SampleApp.Services.Tests
111
+ Affected test classes (1):
112
+ • PricingServiceTests
113
+ dotnet command:
114
+ dotnet test "...SampleApp.Services.Tests.csproj" --filter "FullyQualifiedName~PricingServiceTests"
115
+ ──────────────────────────────────────────────────────────────────
116
+ ```
117
+
118
+ `Status: Targeted run` means the script selected a subset. `Status: RUN ALL TESTS` means it fell back to running everything (expected for Scenario 4).
119
+
120
+ ### Additional scenarios
121
+
122
+ | File to change | Expected result |
123
+ |---|---|
124
+ | `src/SampleApp.Core/Models/Product.cs` | Both test projects, filtered to `ProductTests` + dependent service tests |
125
+ | `tests/SampleApp.Core.Tests/Utilities/StringHelperTests.cs` | `Core.Tests` only, filtered to `StringHelperTests` |
126
+ | `src/SampleApp.Services/appsettings.json` | `Services.Tests`, no class filter (config file) |
127
+ | `src/SampleApp.Services/SampleApp.Services.csproj` | All tests, `run_all = true` (infra file) |
128
+
129
+ ---
130
+
131
+ ## Usage reference
132
+
133
+ ```
134
+ python assess_impact.py [OPTIONS] [-- DOTNET_ARGS]
135
+
136
+ --base REF Git ref to diff against (e.g. HEAD~1, main, origin/main)
137
+ --head REF Head ref to diff from (default: HEAD)
138
+ --root DIR Directory containing .sln / .csproj files (default: .)
139
+ --strategy project | convention | symbol | hybrid (default: hybrid)
140
+ --output, -o human | json | github-actions | azure-devops (default: human)
141
+ --run Execute dotnet test after analysis
142
+ --unstaged Analyse working-tree changes instead of a git diff
143
+ -- Everything after this is forwarded to dotnet test
144
+ ```
145
+
146
+ ### Strategies
147
+
148
+ | Strategy | What it does |
149
+ |---|---|
150
+ | `project` | Parse `.sln`/`.csproj` to find which test projects reference the changed source project (BFS-transitive through the dependency graph) |
151
+ | `convention` | `FooService.cs` → look for `FooServiceTests.cs`, `FooServiceTest.cs`, `TestFooService.cs` |
152
+ | `symbol` | Extract `public class/interface/enum` names from the changed file; grep all test `.cs` files for references |
153
+ | `hybrid` | All three combined (default) |
154
+
155
+ ### Output formats
156
+
157
+ ```bash
158
+ # Human-readable (default)
159
+ python assess_impact.py --base HEAD~1 --root sample-app
160
+
161
+ # JSON — pipe into scripts or CI steps
162
+ python assess_impact.py --base HEAD~1 --root sample-app --output json
163
+
164
+ # GitHub Actions — prints `echo "key=value" >> $GITHUB_OUTPUT` lines
165
+ python assess_impact.py --base HEAD~1 --root sample-app --output github-actions
166
+
167
+ # Azure DevOps — prints `##vso[task.setvariable ...]` lines
168
+ python assess_impact.py --base HEAD~1 --root sample-app --output azure-devops
169
+ ```
170
+
171
+ ### JSON output fields
172
+
173
+ ```jsonc
174
+ {
175
+ "run_all": false, // true = targeted selection was abandoned
176
+ "test_filter": "FullyQualifiedName~PricingServiceTests",
177
+ "test_project_paths": ["...SampleApp.Services.Tests.csproj"],
178
+ "affected_test_projects": ["SampleApp.Services.Tests"],
179
+ "affected_test_classes": ["PricingServiceTests"],
180
+ "dotnet_command": "dotnet test \"...\" --filter \"...\"",
181
+ "reason": "Analysis complete",
182
+ "strategy_notes": [] // warnings / fallback explanations
183
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ## CI integration
189
+
190
+ ### GitHub Actions
191
+
192
+ ```yaml
193
+ - name: Test Impact Analysis
194
+ id: tia
195
+ run: python assess_impact.py --base ${{ github.event.before }} --root sample-app --output github-actions
196
+
197
+ - name: Run affected tests
198
+ if: steps.tia.outputs.has_tests == 'true'
199
+ run: ${{ steps.tia.outputs.dotnet_command }}
200
+ ```
201
+
202
+ Available outputs: `test_filter`, `run_all`, `has_tests`, `test_project_paths`, `dotnet_command`.
203
+
204
+ ### Azure DevOps
205
+
206
+ ```yaml
207
+ - script: python assess_impact.py --base $(System.PullRequest.TargetBranch) --root sample-app --output azure-devops
208
+ displayName: Test Impact Analysis
209
+
210
+ - script: $(dotnetCommand)
211
+ condition: eq(variables['hasTests'], 'true')
212
+ displayName: Run affected tests
213
+ ```
214
+
215
+ Available variables: `testFilter`, `runAllTests`, `hasTests`, `testProjectPaths`, `dotnetCommand`.
216
+
217
+ ---
218
+
219
+ ## How it works
220
+
221
+ ```
222
+ git diff --name-status base..head
223
+
224
+ classify each file → INFRA | IGNORED | CS_SOURCE | CONFIG | UNKNOWN
225
+
226
+ INFRA → run all tests
227
+ IGNORED→ skip
228
+ UNKNOWN→ run all tests (safe fallback)
229
+ CS_SOURCE / CONFIG → run 3 strategies ↓
230
+
231
+ discover_projects() parse .sln + glob .csproj (excludes obj/ bin/)
232
+ build_reverse_deps() BFS: {source_project → set of test projects that cover it}
233
+ ↓ per CS_SOURCE file:
234
+ strategy 1: project dependency graph (ownership → reverse dep lookup)
235
+ strategy 2: convention mapping (FooService → FooServiceTests)
236
+ strategy 3: symbol search (public types → grep cached test files)
237
+ ↓ per CONFIG file:
238
+ find owning project → run its test projects (no class filter)
239
+
240
+ build_filter() FullyQualifiedName~A|FullyQualifiedName~B
241
+ capped at 40 classes; drops to project-level if exceeded
242
+
243
+ ImpactResult → formatter → stdout
244
+ ```
245
+
246
+ **Fallback escalation** — rather than silently skipping tests, the script escalates:
247
+ 1. `.cs` file not owned by any known project → run all test projects
248
+ 2. Source project changed but dependency graph finds no covering tests → run all test projects
249
+ 3. Config file not owned by any project → run all test projects
250
+
251
+ ---
252
+
253
+ ## Extending to other languages
254
+
255
+ To add Python, Node.js, or Java support, provide:
256
+
257
+ 1. A `discover_projects()` equivalent that returns `List[Project]` for that ecosystem
258
+ 2. Three strategy functions matching the signatures of `strategy_dependency_graph`, `strategy_convention`, `strategy_symbol_search`
259
+ 3. New entries in `INFRA_EXTENSIONS`, `IGNORED_EXTENSIONS`, and `CONFIG_EXTENSIONS` for that ecosystem's file types
260
+
261
+ The git analysis, file classification pipeline, output formatters, and CI integration are language-agnostic and require no changes.