diff --git a/.github/workflows/compiler-execution.yml b/.github/workflows/compiler-execution.yml index 8cb20c2..c5b9ba2 100644 --- a/.github/workflows/compiler-execution.yml +++ b/.github/workflows/compiler-execution.yml @@ -17,6 +17,11 @@ jobs: fail-fast: false matrix: runtime: [release-0.4.2, current] + include: + - runtime: release-0.4.2 + worker-version: '0.4.2' + - runtime: current + worker-version: '0.4.3' steps: - name: Check out reviewed validation scenarios uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 @@ -40,7 +45,9 @@ jobs: set -euo pipefail printf 'CFS_RUNTIME_SOURCE=%s\n' "$(git -C runtime rev-parse HEAD)" >> "$GITHUB_ENV" - name: Build worker on disposable hosted runner - run: docker build --label org.opencontainers.image.revision="$CFS_RUNTIME_SOURCE" -t code-flow-worker:0.4.2 runtime/runner + env: + CFS_WORKER_IMAGE: code-flow-worker:${{ matrix.worker-version }} + run: docker build --label org.opencontainers.image.revision="$CFS_RUNTIME_SOURCE" -t "$CFS_WORKER_IMAGE" runtime/runner - name: Run realistic programs and deliberate failure controls working-directory: runtime env: @@ -52,6 +59,12 @@ jobs: --runtime-source "$CFS_RUNTIME_SOURCE" --lane "$CFS_VALIDATION_LANE" --output ../validation/artifacts/compiler-validation + - name: Verify actual C# target exits and collector failures + if: matrix.runtime == 'current' + working-directory: runtime + env: + PYTHONPATH: ${{ github.workspace }}/runtime:${{ github.workspace }}/validation + run: python -m scripts.csharp_coverage_checks --runtime-source "$CFS_RUNTIME_SOURCE" --output ../validation/artifacts/csharp-coverage - name: Test the actual published frozen 0.4.2 service if: matrix.runtime == 'release-0.4.2' working-directory: validation @@ -72,5 +85,6 @@ jobs: validation/artifacts/compiler-validation/results.json validation/artifacts/compiler-validation/summary.md validation/artifacts/released-service/*.json + validation/artifacts/csharp-coverage/*.json if-no-files-found: error retention-days: 3 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d7553ff..3ca40b4 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -93,7 +93,7 @@ jobs: with: python-version: '3.12' - run: pip install -e '.[dev]' - - run: docker build -t code-flow-worker:0.4.2 runner + - run: docker build -t code-flow-worker:0.4.3 runner - run: python -m scripts.worker_checks - run: python -m scripts.private_worker_checks - run: python -m scripts.push_analysis diff --git a/README.md b/README.md index 95536c4..826f16c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An independently written successor to a group AI-in-SDLC capstone: a local-first development workbench for isolated polyglot checks, configurable AI proposals, and replayable revision evidence. -**Version 0.4.2: bounded reports, safe shutdown and corrected draft verification.** Create an account, unlock your encrypted source/history workspace, and use the same interface in the desktop app or your browser. Filter diagnostics by file or unused-code category, select an exact line, inspect its read-only evidence snapshot, and open the editable draft only when its SHA-256 still matches. No public backend, shared login, analytics, ads or cloud AI calls are enabled by default. By [Jason Stys](https://github.com/JasonStys); [contact via LinkedIn](https://www.linkedin.com/in/jason-stys-31b30b351/). +**Source version 0.4.3: exact instrumented C# exits.** The worker now separates the program's real exit status from the coverage collector, so approved nonzero returns can have fully measured coverage. Read the [fix and validation](docs/csharp-coverage.md). Create an account, unlock your encrypted source/history workspace, and use the same interface in the desktop app or your browser. Filter diagnostics by file or unused-code category, select an exact line, inspect its read-only evidence snapshot, and open the editable draft only when its SHA-256 still matches. No public backend, shared login, analytics, ads or cloud AI calls are enabled by default. By [Jason Stys](https://github.com/JasonStys); [contact via LinkedIn](https://www.linkedin.com/in/jason-stys-31b30b351/). Download OS-specific installers/portable packages from [Releases](https://github.com/JasonStys/code-flow-studio/releases). Read [private-account operation](docs/private-accounts.md), [current validation](docs/validation-polyglot.md), [historical Python-location validation](docs/validation-locations.md) and [security boundaries](docs/security.md). Packages are unsigned. This is a local development tool, not a public hostile-code SaaS sandbox or an autonomous correctness oracle. @@ -16,7 +16,7 @@ Download OS-specific installers/portable packages from [Releases](https://github ## Hosted compiler acceptance -The [realistic compiler suite](docs/compiler-execution.md) covers all twelve executable adapters with 72 independently authored input/output cases, deliberate compiler/runtime/wrong-answer controls, exact unused/uncovered anchors, and encrypted private-report replay. Its release lane identifies the published 0.4.2 source and tests the actual published Linux frozen service; a second lane tests the current checkout. Read the [dated observations and limits](docs/reports/hosted-compilers-2026-11-10.md) before interpreting a green run. This hosted validation does not install, repair or reset Docker on an end user's PC. +The [realistic compiler suite](docs/compiler-execution.md) covers all twelve executable adapters with 72 independently authored input/output cases, deliberate compiler/runtime/wrong-answer controls, exact unused/uncovered anchors, and encrypted private-report replay. Its historical lane preserves the published 0.4.2 baseline, including its C# limitation; the current 0.4.3 lane requires the corrected target/collector protocol and fully measured approved C# cases. Read the [0.4.2 observations](docs/reports/hosted-compilers-2026-11-10.md) and [0.4.3 validation](docs/reports/csharp-coverage-2026-11-10.md). Published 0.4.2 packages are unchanged; this source fix neither installs an app nor alters Docker on an end user's PC. ## Features and limits diff --git a/docs/README.md b/docs/README.md index e414332..319e2a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,7 @@ # Documentation +- [0.4.3 exact C# coverage exits](csharp-coverage.md), [release notes](releases/v0.4.3.md) and [validation](reports/csharp-coverage-2026-11-10.md). + - [Verified source inspection and draft navigation](source-navigation.md), with [observed practical-workflow validation](reports/practical-workflows-2026-11-09.md). - [Worker sanitizer compatibility and separate analyzer audit](worker-toolchain.md). - [0.4.2 delivery correction](releases/v0.4.2.md): required draft visibility for the read-only final verifier; coherent new product/worker metadata and preserved prior assets. diff --git a/docs/code-index.md b/docs/code-index.md index e0600ac..788cf23 100644 --- a/docs/code-index.md +++ b/docs/code-index.md @@ -122,304 +122,400 @@ Generated declaration locations. Python uses AST; JS/TS uses declaration-line ma |---|---|---| | variable | `port` | 9 | +## runner/csharp_coverage_bridge.py + +| Kind | Declaration | Line | +|---|---|---| +| variable | `ANALYSIS` | 35 | +| variable | `DOTNET` | 36 | +| variable | `ASSEMBLY` | 37 | +| variable | `MAX_INPUT` | 38 | +| variable | `MAX_JSON` | 39 | +| variable | `MAX_BRIDGE` | 40 | +| variable | `REQUEST_FIELDS` | 41 | +| variable | `OBSERVATION_FIELDS` | 42 | +| function | `_require(condition, message)` | 45 | +| parameter | `condition` | 45 | +| parameter | `message` | 45 | +| function | `_hash(value)` | 51 | +| parameter | `value` | 51 | +| function | `_hex(value, length)` | 56 | +| parameter | `length` | 56 | +| parameter | `value` | 56 | +| function | `_directory(path)` | 61 | +| parameter | `path` | 61 | +| variable | `component` | 64 | +| variable | `metadata` | 65 | +| function | `_root(root)` | 72 | +| parameter | `root` | 72 | +| function | `_missing(path)` | 78 | +| parameter | `path` | 78 | +| function | `_regular(metadata)` | 87 | +| parameter | `metadata` | 87 | +| function | `_read(path, maximum)` | 95 | +| parameter | `maximum` | 95 | +| parameter | `path` | 95 | +| variable | `before` | 97 | +| variable | `flags` | 100 | +| variable | `stream` | 103 | +| variable | `opened` | 104 | +| variable | `body` | 110 | +| variable | `after` | 111 | +| function | `_json(path)` | 123 | +| parameter | `path` | 123 | +| function | `pairs(items)` | 126 | +| parameter | `items` | 126 | +| variable | `value` | 127 | +| function | `nonfinite(_value)` | 131 | +| parameter | `_value` | 131 | +| variable | `value` | 134 | +| function | `_request(path)` | 143 | +| parameter | `path` | 143 | +| variable | `match` | 147 | +| variable | `value` | 149 | +| function | `_validate_request(value, suffix)` | 154 | +| parameter | `suffix` | 154 | +| parameter | `value` | 154 | +| function | `_publish(path, value)` | 172 | +| parameter | `path` | 172 | +| parameter | `value` | 172 | +| variable | `body` | 176 | +| variable | `temporary` | 178 | +| variable | `flags` | 179 | +| variable | `descriptor` | 180 | +| variable | `stream` | 182 | +| function | `_bridge_hash()` | 191 | +| function | `make_request(root, source_sha256, case_index, stdin_bytes)` | 196 | +| parameter | `case_index` | 196 | +| parameter | `root` | 196 | +| parameter | `source_sha256` | 196 | +| parameter | `stdin_bytes` | 196 | +| variable | `suffix` | 207 | +| variable | `path` | 208 | +| variable | `value` | 209 | +| function | `read_observation(request_path, request)` | 224 | +| parameter | `request` | 224 | +| parameter | `request_path` | 224 | +| variable | `actual` | 226 | +| variable | `suffix` | 229 | +| variable | `value` | 231 | +| variable | `echoed` | 233 | +| variable | `name` | 233 | +| variable | `name` | 236 | +| function | `observe(request_path, input_stream)` | 254 | +| parameter | `input_stream` | 254 | +| parameter | `request_path` | 254 | +| variable | `request` | 262 | +| variable | `receipt` | 263 | +| variable | `raw` | 269 | +| variable | `completed` | 277 | +| variable | `name` | 283 | +| variable | `value` | 283 | +| function | `main()` | 293 | + ## runner/language_checks.py | Kind | Declaration | Line | |---|---|---| -| variable | `MAX_REPORT` | 24 | -| variable | `SUFFIXES` | 25 | -| function | `read_text(path)` | 43 | -| parameter | `path` | 43 | -| function | `read_json(path)` | 50 | -| parameter | `path` | 50 | -| function | `read_xml(path)` | 55 | -| parameter | `path` | 55 | -| variable | `text` | 59 | -| variable | `text` | 62 | -| class | `Evidence` | 68 | -| function | `__init__(self, project)` | 71 | -| parameter | `project` | 71 | -| parameter | `self` | 71 | -| variable | `file` | 75 | -| variable | `name` | 82 | -| variable | `text` | 82 | -| variable | `name` | 84 | -| variable | `text` | 84 | -| function | `name(self, path)` | 90 | -| parameter | `path` | 90 | -| parameter | `self` | 90 | -| variable | `value` | 92 | -| function | `line(self, name, value)` | 95 | -| parameter | `name` | 95 | -| parameter | `self` | 95 | -| parameter | `value` | 95 | -| variable | `line` | 97 | -| function | `add_lines(self, path, counts)` | 100 | -| parameter | `counts` | 100 | -| parameter | `path` | 100 | -| parameter | `self` | 100 | -| variable | `name` | 102 | -| variable | `lines` | 105 | -| variable | `count` | 106 | -| variable | `value` | 106 | -| variable | `line` | 107 | -| function | `finding(self, path, line, column, code, message, severity)` | 111 | -| parameter | `code` | 111 | -| parameter | `column` | 111 | -| parameter | `line` | 111 | -| parameter | `message` | 111 | -| parameter | `path` | 111 | -| parameter | `self` | 111 | -| parameter | `severity` | 111 | -| variable | `name` | 113 | -| variable | `key` | 116 | -| function | `coverage(self, tool, metric, cases, expected)` | 136 | -| parameter | `cases` | 136 | -| parameter | `expected` | 136 | -| parameter | `metric` | 136 | -| parameter | `self` | 136 | -| parameter | `tool` | 136 | -| variable | `files` | 138 | -| variable | `name` | 139 | -| variable | `text` | 139 | -| variable | `counts` | 140 | -| variable | `covered` | 147 | -| variable | `value` | 147 | -| variable | `count` | 158 | -| variable | `line` | 158 | -| variable | `count` | 159 | -| variable | `line` | 159 | -| variable | `budget` | 165 | -| variable | `key` | 166 | -| variable | `record` | 167 | -| variable | `values` | 168 | -| variable | `budget` | 170 | -| variable | `file` | 172 | -| variable | `measured` | 172 | -| variable | `complete` | 173 | -| variable | `case` | 175 | -| variable | `file` | 185 | -| function | `cobertura(evidence, path, case_index)` | 190 | -| parameter | `case_index` | 190 | -| parameter | `evidence` | 190 | -| parameter | `path` | 190 | -| variable | `document` | 192 | -| variable | `roots` | 193 | -| variable | `source` | 193 | -| variable | `item` | 194 | -| variable | `reported` | 195 | -| variable | `paths` | 196 | -| variable | `root` | 196 | -| variable | `candidate` | 197 | -| variable | `name` | 197 | -| variable | `path` | 197 | -| variable | `line` | 201 | -| variable | `line` | 203 | -| variable | `condition` | 204 | -| function | `jacoco(evidence, path)` | 216 | -| parameter | `evidence` | 216 | -| parameter | `path` | 216 | -| variable | `file` | 218 | -| variable | `name` | 219 | -| variable | `line` | 222 | -| variable | `line` | 230 | -| function | `gcov(evidence, path)` | 235 | -| parameter | `evidence` | 235 | -| parameter | `path` | 235 | -| variable | `stream` | 237 | -| variable | `text` | 238 | -| variable | `file` | 241 | -| variable | `name` | 242 | -| variable | `line` | 245 | -| variable | `line` | 248 | -| variable | `branch` | 249 | -| variable | `index` | 249 | -| function | `lcov(evidence, text)` | 253 | -| parameter | `evidence` | 253 | -| parameter | `text` | 253 | -| variable | `name` | 255 | -| variable | `row` | 256 | -| variable | `name` | 258 | -| variable | `_` | 260 | -| variable | `count` | 260 | -| variable | `line` | 260 | -| variable | `block` | 263 | -| variable | `branch` | 263 | -| variable | `count` | 263 | -| variable | `line` | 263 | -| function | `shell_lines(node)` | 274 | -| parameter | `node` | 274 | -| variable | `output` | 276 | -| variable | `pending` | 277 | -| variable | `item` | 279 | -| variable | `position` | 282 | -| function | `assembly_symbols(evidence)` | 291 | -| parameter | `evidence` | 291 | -| variable | `name` | 293 | -| variable | `text` | 293 | -| variable | `definitions` | 294 | -| variable | `exported` | 295 | -| variable | `used` | 296 | -| variable | `line` | 297 | -| variable | `source` | 297 | -| variable | `source` | 298 | -| variable | `label` | 304 | +| variable | `MAX_REPORT` | 30 | +| variable | `SUFFIXES` | 31 | +| function | `read_text(path)` | 49 | +| parameter | `path` | 49 | +| function | `read_json(path)` | 56 | +| parameter | `path` | 56 | +| function | `read_xml(path)` | 61 | +| parameter | `path` | 61 | +| variable | `text` | 65 | +| variable | `text` | 68 | +| class | `Evidence` | 74 | +| function | `__init__(self, project)` | 77 | +| parameter | `project` | 77 | +| parameter | `self` | 77 | +| variable | `file` | 81 | +| variable | `name` | 88 | +| variable | `text` | 88 | +| variable | `name` | 90 | +| variable | `text` | 90 | +| function | `name(self, path)` | 96 | +| parameter | `path` | 96 | +| parameter | `self` | 96 | +| variable | `value` | 98 | +| function | `line(self, name, value)` | 101 | +| parameter | `name` | 101 | +| parameter | `self` | 101 | +| parameter | `value` | 101 | +| variable | `line` | 103 | +| function | `add_lines(self, path, counts)` | 106 | +| parameter | `counts` | 106 | +| parameter | `path` | 106 | +| parameter | `self` | 106 | +| variable | `name` | 108 | +| variable | `lines` | 111 | +| variable | `count` | 112 | +| variable | `value` | 112 | +| variable | `line` | 113 | +| function | `finding(self, path, line, column, code, message, severity)` | 117 | +| parameter | `code` | 117 | +| parameter | `column` | 117 | +| parameter | `line` | 117 | +| parameter | `message` | 117 | +| parameter | `path` | 117 | +| parameter | `self` | 117 | +| parameter | `severity` | 117 | +| variable | `name` | 119 | +| variable | `key` | 122 | +| function | `coverage(self, tool, metric, cases, expected)` | 142 | +| parameter | `cases` | 142 | +| parameter | `expected` | 142 | +| parameter | `metric` | 142 | +| parameter | `self` | 142 | +| parameter | `tool` | 142 | +| variable | `files` | 144 | +| variable | `name` | 145 | +| variable | `text` | 145 | +| variable | `counts` | 146 | +| variable | `covered` | 153 | +| variable | `value` | 153 | +| variable | `count` | 164 | +| variable | `line` | 164 | +| variable | `count` | 165 | +| variable | `line` | 165 | +| variable | `budget` | 171 | +| variable | `key` | 172 | +| variable | `record` | 173 | +| variable | `values` | 174 | +| variable | `budget` | 176 | +| variable | `file` | 178 | +| variable | `measured` | 178 | +| variable | `complete` | 179 | +| variable | `case` | 184 | +| variable | `file` | 194 | +| function | `cobertura(evidence, path, case_index)` | 199 | +| parameter | `case_index` | 199 | +| parameter | `evidence` | 199 | +| parameter | `path` | 199 | +| variable | `document` | 201 | +| variable | `roots` | 202 | +| variable | `source` | 202 | +| variable | `item` | 203 | +| variable | `reported` | 204 | +| variable | `paths` | 205 | +| variable | `root` | 205 | +| variable | `candidate` | 206 | +| variable | `name` | 206 | +| variable | `path` | 206 | +| variable | `line` | 210 | +| variable | `line` | 212 | +| variable | `condition` | 213 | +| function | `jacoco(evidence, path)` | 225 | +| parameter | `evidence` | 225 | +| parameter | `path` | 225 | +| variable | `file` | 227 | +| variable | `name` | 228 | +| variable | `line` | 231 | +| variable | `line` | 239 | +| function | `gcov(evidence, path)` | 244 | +| parameter | `evidence` | 244 | +| parameter | `path` | 244 | +| variable | `stream` | 246 | +| variable | `text` | 247 | +| variable | `file` | 250 | +| variable | `name` | 251 | +| variable | `line` | 254 | +| variable | `line` | 257 | +| variable | `branch` | 258 | +| variable | `index` | 258 | +| function | `lcov(evidence, text)` | 262 | +| parameter | `evidence` | 262 | +| parameter | `text` | 262 | +| variable | `name` | 264 | +| variable | `row` | 265 | +| variable | `name` | 267 | +| variable | `_` | 269 | +| variable | `count` | 269 | +| variable | `line` | 269 | +| variable | `block` | 272 | +| variable | `branch` | 272 | +| variable | `count` | 272 | +| variable | `line` | 272 | +| function | `shell_lines(node)` | 283 | +| parameter | `node` | 283 | +| variable | `output` | 285 | +| variable | `pending` | 286 | +| variable | `item` | 288 | +| variable | `position` | 291 | +| function | `assembly_symbols(evidence)` | 300 | +| parameter | `evidence` | 300 | +| variable | `name` | 302 | +| variable | `text` | 302 | +| variable | `definitions` | 303 | +| variable | `exported` | 304 | +| variable | `used` | 305 | +| variable | `line` | 306 | +| variable | `source` | 306 | | variable | `source` | 307 | -| variable | `source` | 309 | -| variable | `column` | 311 | -| variable | `label` | 311 | -| variable | `line` | 311 | -| function | `native_checks(project, work, execute, command)` | 322 | -| parameter | `command` | 322 | -| parameter | `execute` | 322 | -| parameter | `project` | 322 | -| parameter | `work` | 322 | -| variable | `evidence` | 324 | -| variable | `root` | 325 | -| variable | `unused` | 327 | -| variable | `coverage` | 331 | -| variable | `coverage` | 333 | -| function | `unused_checks(evidence, root, execute)` | 340 | -| parameter | `evidence` | 340 | -| parameter | `execute` | 340 | -| parameter | `root` | 340 | -| variable | `project` | 342 | -| variable | `language` | 343 | -| variable | `entry` | 344 | -| variable | `output` | 345 | -| variable | `allowed` | 346 | -| variable | `result` | 347 | -| variable | `parse_failed` | 348 | -| variable | `result` | 351 | -| variable | `item` | 356 | +| variable | `label` | 313 | +| variable | `source` | 316 | +| variable | `source` | 318 | +| variable | `column` | 320 | +| variable | `label` | 320 | +| variable | `line` | 320 | +| function | `native_checks(project, work, execute, command)` | 331 | +| parameter | `command` | 331 | +| parameter | `execute` | 331 | +| parameter | `project` | 331 | +| parameter | `work` | 331 | +| variable | `evidence` | 333 | +| variable | `root` | 334 | +| variable | `unused` | 336 | +| variable | `coverage` | 340 | +| variable | `coverage` | 342 | +| function | `unused_checks(evidence, root, execute)` | 349 | +| parameter | `evidence` | 349 | +| parameter | `execute` | 349 | +| parameter | `root` | 349 | +| variable | `project` | 351 | +| variable | `language` | 352 | +| variable | `entry` | 353 | +| variable | `output` | 354 | +| variable | `allowed` | 355 | +| variable | `result` | 356 | | variable | `parse_failed` | 357 | -| variable | `tool` | 366 | -| variable | `result` | 368 | -| variable | `item` | 384 | -| variable | `location` | 387 | -| variable | `tool` | 395 | -| variable | `result` | 397 | -| variable | `line` | 415 | -| variable | `item` | 416 | -| variable | `code` | 417 | -| variable | `span` | 419 | -| variable | `tool` | 428 | -| variable | `allowed` | 430 | -| variable | `result` | 431 | -| variable | `parsed` | 448 | -| variable | `parse_failed` | 449 | -| variable | `file` | 450 | -| variable | `item` | 451 | -| variable | `tool` | 459 | -| variable | `allowed` | 461 | -| variable | `result` | 462 | -| variable | `parsed` | 479 | -| variable | `parse_failed` | 480 | -| variable | `file` | 481 | -| variable | `item` | 482 | -| variable | `tool` | 484 | -| variable | `allowed` | 486 | -| variable | `result` | 487 | -| variable | `item` | 499 | -| variable | `parse_failed` | 500 | -| variable | `tool` | 504 | -| variable | `allowed` | 506 | -| variable | `result` | 507 | -| variable | `file` | 524 | -| variable | `item` | 525 | -| variable | `parse_failed` | 526 | -| variable | `tool` | 534 | -| variable | `result` | 536 | -| variable | `line` | 552 | -| variable | `match` | 553 | -| variable | `tool` | 558 | -| variable | `result` | 561 | -| variable | `tool` | 562 | -| function | `measure(evidence, root, execute, command)` | 585 | -| parameter | `command` | 585 | -| parameter | `evidence` | 585 | -| parameter | `execute` | 585 | -| parameter | `root` | 585 | -| variable | `project` | 587 | -| variable | `entry` | 588 | -| variable | `language` | 588 | -| variable | `work` | 589 | -| variable | `build` | 590 | -| variable | `metric` | 591 | -| variable | `tool` | 591 | -| variable | `run` | 592 | -| variable | `metric` | 594 | -| variable | `tool` | 594 | -| variable | `build` | 595 | -| variable | `run` | 605 | -| variable | `metric` | 607 | -| variable | `tool` | 607 | -| variable | `build` | 608 | -| variable | `run` | 623 | -| variable | `metric` | 625 | -| variable | `tool` | 625 | -| variable | `classes` | 626 | -| variable | `build` | 628 | -| variable | `run` | 629 | -| variable | `metric` | 638 | -| variable | `tool` | 638 | -| variable | `build` | 639 | -| variable | `metric` | 641 | -| variable | `tool` | 641 | -| variable | `metric` | 644 | -| variable | `tool` | 644 | -| variable | `run` | 645 | +| variable | `result` | 360 | +| variable | `item` | 365 | +| variable | `parse_failed` | 366 | +| variable | `tool` | 375 | +| variable | `result` | 377 | +| variable | `item` | 393 | +| variable | `location` | 396 | +| variable | `tool` | 404 | +| variable | `result` | 406 | +| variable | `line` | 424 | +| variable | `item` | 425 | +| variable | `code` | 426 | +| variable | `span` | 428 | +| variable | `tool` | 437 | +| variable | `allowed` | 439 | +| variable | `result` | 440 | +| variable | `parsed` | 457 | +| variable | `parse_failed` | 458 | +| variable | `file` | 459 | +| variable | `item` | 460 | +| variable | `tool` | 468 | +| variable | `allowed` | 470 | +| variable | `result` | 471 | +| variable | `parsed` | 488 | +| variable | `parse_failed` | 489 | +| variable | `file` | 490 | +| variable | `item` | 491 | +| variable | `tool` | 493 | +| variable | `allowed` | 495 | +| variable | `result` | 496 | +| variable | `item` | 508 | +| variable | `parse_failed` | 509 | +| variable | `tool` | 513 | +| variable | `allowed` | 515 | +| variable | `result` | 516 | +| variable | `file` | 533 | +| variable | `item` | 534 | +| variable | `parse_failed` | 535 | +| variable | `tool` | 543 | +| variable | `result` | 545 | +| variable | `line` | 561 | +| variable | `match` | 562 | +| variable | `tool` | 567 | +| variable | `result` | 570 | +| variable | `tool` | 571 | +| function | `measure(evidence, root, execute, command)` | 594 | +| parameter | `command` | 594 | +| parameter | `evidence` | 594 | +| parameter | `execute` | 594 | +| parameter | `root` | 594 | +| variable | `project` | 596 | +| variable | `entry` | 597 | +| variable | `language` | 597 | +| variable | `work` | 598 | +| variable | `build` | 599 | +| variable | `metric` | 600 | +| variable | `tool` | 600 | +| variable | `run` | 601 | +| variable | `metric` | 603 | +| variable | `tool` | 603 | +| variable | `build` | 604 | +| variable | `run` | 614 | +| variable | `metric` | 616 | +| variable | `tool` | 616 | +| variable | `build` | 617 | +| variable | `run` | 632 | +| variable | `metric` | 634 | +| variable | `tool` | 634 | +| variable | `classes` | 635 | +| variable | `build` | 637 | +| variable | `run` | 638 | | variable | `metric` | 647 | | variable | `tool` | 647 | | variable | `build` | 648 | -| variable | `metric` | 660 | -| variable | `tool` | 660 | -| variable | `name` | 661 | -| variable | `tree` | 662 | -| variable | `result` | 663 | -| variable | `line` | 670 | -| variable | `metric` | 672 | -| variable | `tool` | 672 | -| variable | `result` | 674 | -| variable | `cases` | 677 | -| variable | `case` | 678 | -| variable | `index` | 678 | -| variable | `output` | 679 | -| variable | `environment` | 680 | -| variable | `run` | 688 | -| variable | `run` | 690 | -| variable | `run` | 701 | -| variable | `result` | 716 | -| variable | `lines` | 730 | -| variable | `name` | 730 | -| variable | `count` | 731 | -| variable | `line` | 731 | -| variable | `trace` | 735 | -| variable | `match` | 736 | -| variable | `current` | 739 | -| variable | `row` | 740 | -| variable | `current` | 742 | -| variable | `_` | 744 | -| variable | `hits` | 744 | -| variable | `line` | 744 | -| variable | `output` | 747 | -| variable | `result` | 748 | -| variable | `lines` | 754 | -| variable | `name` | 754 | -| variable | `note` | 757 | -| variable | `result` | 758 | -| variable | `file` | 761 | -| variable | `profiles` | 764 | -| variable | `result` | 765 | -| variable | `output` | 778 | -| variable | `result` | 779 | -| variable | `output` | 794 | -| variable | `result` | 795 | -| variable | `result` | 816 | -| variable | `row` | 817 | -| variable | `match` | 818 | -| variable | `name` | 820 | -| variable | `existing` | 822 | +| variable | `metric` | 650 | +| variable | `tool` | 650 | +| variable | `metric` | 653 | +| variable | `tool` | 653 | +| variable | `run` | 654 | +| variable | `metric` | 656 | +| variable | `tool` | 656 | +| variable | `build` | 657 | +| variable | `metric` | 669 | +| variable | `tool` | 669 | +| variable | `name` | 670 | +| variable | `tree` | 671 | +| variable | `result` | 672 | +| variable | `line` | 679 | +| variable | `metric` | 681 | +| variable | `tool` | 681 | +| variable | `result` | 683 | +| variable | `cases` | 686 | +| variable | `project_sha256` | 687 | +| variable | `case` | 692 | +| variable | `index` | 692 | +| variable | `output` | 693 | +| variable | `environment` | 694 | +| variable | `run` | 702 | +| variable | `run` | 704 | +| variable | `bridge_request` | 715 | +| variable | `request_path` | 715 | +| variable | `run` | 718 | +| variable | `result` | 733 | +| variable | `observation` | 734 | +| variable | `observation` | 740 | +| variable | `lines` | 764 | +| variable | `name` | 764 | +| variable | `count` | 765 | +| variable | `line` | 765 | +| variable | `trace` | 769 | +| variable | `match` | 770 | +| variable | `current` | 773 | +| variable | `row` | 774 | +| variable | `current` | 776 | +| variable | `_` | 778 | +| variable | `hits` | 778 | +| variable | `line` | 778 | +| variable | `output` | 781 | +| variable | `result` | 782 | +| variable | `lines` | 788 | +| variable | `name` | 788 | +| variable | `note` | 791 | +| variable | `result` | 792 | +| variable | `file` | 795 | +| variable | `profiles` | 798 | +| variable | `result` | 799 | +| variable | `output` | 812 | +| variable | `result` | 813 | +| variable | `output` | 828 | +| variable | `result` | 829 | +| variable | `result` | 850 | +| variable | `row` | 851 | +| variable | `match` | 852 | +| variable | `name` | 854 | +| variable | `existing` | 856 | ## runner/node_checks.cjs @@ -740,174 +836,237 @@ Generated declaration locations. Python uses AST; JS/TS uses declaration-line ma |---|---|---| | variable | `RELEASE_SOURCE` | 40 | | variable | `RELEASE_WORKER_SHA256` | 41 | -| function | `require(condition, message)` | 44 | -| parameter | `condition` | 44 | -| parameter | `message` | 44 | -| function | `source_digest(text)` | 50 | -| parameter | `text` | 50 | -| function | `source_line_count(text)` | 55 | -| parameter | `text` | 55 | -| function | `validate_coverage_completion(coverage, project)` | 60 | -| parameter | `coverage` | 60 | -| parameter | `project` | 60 | -| variable | `wrapped` | 62 | -| variable | `cases` | 64 | -| variable | `actual` | 69 | -| variable | `expected` | 69 | -| variable | `wrapped` | 85 | -| variable | `expected_status` | 86 | -| function | `validate_locations(result, project)` | 93 | -| parameter | `project` | 93 | -| parameter | `result` | 93 | -| variable | `file` | 95 | -| variable | `sources` | 95 | -| variable | `item` | 97 | -| variable | `line` | 98 | -| variable | `name` | 98 | -| function | `validate_positive(result, project)` | 106 | -| parameter | `project` | 106 | -| parameter | `result` | 106 | -| variable | `compilation` | 109 | -| variable | `checks` | 116 | -| variable | `observed` | 118 | -| variable | `actual` | 120 | -| variable | `expected` | 120 | -| variable | `oracle` | 130 | -| variable | `case` | 131 | -| variable | `benchmark` | 135 | -| variable | `samples` | 136 | -| variable | `value` | 139 | -| variable | `coverage` | 145 | -| variable | `entry` | 148 | -| variable | `content` | 149 | -| variable | `file` | 149 | -| variable | `measured` | 155 | -| variable | `name` | 155 | -| variable | `file` | 158 | -| variable | `source` | 158 | -| variable | `line` | 163 | -| variable | `executed` | 165 | -| variable | `missing` | 165 | -| variable | `summary` | 171 | -| variable | `covered` | 172 | -| variable | `extra` | 180 | -| variable | `first` | 184 | -| function | `negative_project(project, kind)` | 207 | -| parameter | `kind` | 207 | -| parameter | `project` | 207 | -| variable | `data` | 209 | -| variable | `replacement` | 215 | -| function | `validate_negative(result, project, kind)` | 226 | -| parameter | `kind` | 226 | -| parameter | `project` | 226 | -| parameter | `result` | 226 | -| variable | `expected_oracle` | 237 | -| variable | `case` | 238 | -| variable | `checks` | 241 | -| variable | `line` | 259 | -| variable | `item` | 265 | -| variable | `case` | 276 | -| variable | `exit_code` | 283 | -| variable | `marker` | 288 | -| class | `Evidence` | 305 | -| function | `__init__(self, output, provenance)` | 308 | -| parameter | `output` | 308 | -| parameter | `provenance` | 308 | -| parameter | `self` | 308 | -| function | `record(self, language, kind, project, result, error)` | 315 | -| parameter | `error` | 316 | -| parameter | `kind` | 316 | -| parameter | `language` | 316 | -| parameter | `project` | 316 | -| parameter | `result` | 316 | -| parameter | `self` | 316 | -| variable | `row` | 319 | -| variable | `key` | 333 | -| function | `save(self)` | 337 | -| parameter | `self` | 337 | -| variable | `record` | 339 | -| variable | `temporary` | 340 | -| variable | `lines` | 343 | -| variable | `row` | 351 | -| variable | `checks` | 352 | -| variable | `lines` | 356 | -| function | `broker_scenarios(evidence)` | 364 | -| parameter | `evidence` | 364 | -| variable | `language` | 366 | -| variable | `baseline` | 367 | -| variable | `kind` | 368 | -| variable | `project` | 371 | -| variable | `result` | 372 | -| variable | `error` | 373 | -| variable | `result` | 375 | -| variable | `error` | 381 | -| function | `anchor_scenarios(evidence)` | 385 | -| parameter | `evidence` | 385 | -| variable | `language` | 387 | -| variable | `missing_line` | 388 | -| variable | `project` | 388 | -| variable | `unused_line` | 388 | -| variable | `result` | 389 | -| variable | `error` | 390 | -| variable | `result` | 392 | -| variable | `item` | 403 | -| variable | `error` | 408 | -| function | `private_scenarios(evidence)` | 412 | -| parameter | `evidence` | 412 | -| variable | `credentials` | 414 | -| variable | `key` | 415 | -| variable | `directory` | 416 | -| variable | `app` | 417 | -| variable | `client` | 422 | -| variable | `jobs` | 430 | -| variable | `language` | 431 | -| variable | `project` | 432 | -| variable | `result` | 433 | -| variable | `error` | 434 | -| variable | `response` | 436 | -| variable | `revision` | 438 | -| variable | `response` | 439 | -| variable | `job` | 441 | -| variable | `deadline` | 442 | -| variable | `response` | 444 | -| variable | `report` | 446 | -| variable | `result` | 451 | -| variable | `exported` | 453 | -| variable | `html` | 458 | -| variable | `error` | 465 | -| variable | `archives` | 469 | -| variable | `response` | 484 | -| variable | `job` | 490 | -| variable | `result_sha256` | 490 | -| variable | `response` | 491 | -| variable | `exported` | 498 | -| function | `provenance(runtime, expected_source, lane)` | 516 | -| parameter | `expected_source` | 516 | -| parameter | `lane` | 516 | -| parameter | `runtime` | 516 | -| variable | `runtime` | 524 | -| variable | `source` | 529 | -| variable | `dirty` | 531 | -| variable | `worker` | 540 | -| variable | `command` | 545 | -| variable | `image` | 546 | -| function | `toolchain_versions()` | 570 | -| variable | `commands` | 572 | -| variable | `program` | 586 | -| variable | `name` | 591 | -| variable | `directory` | 592 | -| variable | `root` | 593 | -| variable | `argv` | 595 | -| variable | `argv` | 597 | -| variable | `code` | 599 | -| variable | `output` | 599 | -| variable | `versions` | 601 | -| variable | `value` | 607 | -| function | `main()` | 615 | -| variable | `parser` | 617 | -| variable | `args` | 622 | -| variable | `evidence` | 623 | -| variable | `language` | 644 | +| variable | `CSHARP_TARGET` | 42 | +| function | `require(condition, message)` | 45 | +| parameter | `condition` | 45 | +| parameter | `message` | 45 | +| function | `source_digest(text)` | 51 | +| parameter | `text` | 51 | +| function | `source_line_count(text)` | 56 | +| parameter | `text` | 56 | +| function | `validate_coverage_completion(coverage, project, historical_source)` | 61 | +| parameter | `coverage` | 62 | +| parameter | `historical_source` | 62 | +| parameter | `project` | 62 | +| variable | `wrapped` | 66 | +| variable | `cases` | 68 | +| variable | `actual` | 73 | +| variable | `expected` | 73 | +| variable | `index` | 73 | +| variable | `wrapped` | 90 | +| variable | `bridge` | 100 | +| variable | `source_hash` | 101 | +| variable | `file` | 103 | +| variable | `input_bytes` | 109 | +| variable | `expected_bridge` | 110 | +| variable | `expected_status` | 129 | +| function | `validate_locations(result, project)` | 136 | +| parameter | `project` | 136 | +| parameter | `result` | 136 | +| variable | `file` | 138 | +| variable | `sources` | 138 | +| variable | `item` | 140 | +| variable | `line` | 141 | +| variable | `name` | 141 | +| function | `validate_positive(result, project, historical_source)` | 149 | +| parameter | `historical_source` | 149 | +| parameter | `project` | 149 | +| parameter | `result` | 149 | +| variable | `compilation` | 152 | +| variable | `checks` | 159 | +| variable | `observed` | 161 | +| variable | `actual` | 163 | +| variable | `expected` | 163 | +| variable | `oracle` | 173 | +| variable | `case` | 174 | +| variable | `benchmark` | 178 | +| variable | `samples` | 179 | +| variable | `value` | 182 | +| variable | `coverage` | 188 | +| variable | `entry` | 191 | +| variable | `content` | 192 | +| variable | `file` | 192 | +| variable | `measured` | 198 | +| variable | `name` | 198 | +| variable | `file` | 201 | +| variable | `source` | 201 | +| variable | `line` | 206 | +| variable | `executed` | 208 | +| variable | `missing` | 208 | +| variable | `summary` | 214 | +| variable | `covered` | 215 | +| variable | `extra` | 223 | +| variable | `first` | 227 | +| function | `negative_project(project, kind)` | 250 | +| parameter | `kind` | 250 | +| parameter | `project` | 250 | +| variable | `data` | 252 | +| variable | `replacement` | 258 | +| function | `validate_negative(result, project, kind)` | 269 | +| parameter | `kind` | 269 | +| parameter | `project` | 269 | +| parameter | `result` | 269 | +| variable | `expected_oracle` | 280 | +| variable | `case` | 281 | +| variable | `checks` | 284 | +| variable | `line` | 302 | +| variable | `item` | 308 | +| variable | `case` | 319 | +| variable | `exit_code` | 326 | +| variable | `marker` | 331 | +| class | `Evidence` | 348 | +| function | `__init__(self, output, provenance)` | 351 | +| parameter | `output` | 351 | +| parameter | `provenance` | 351 | +| parameter | `self` | 351 | +| function | `record(self, language, kind, project, result, error)` | 358 | +| parameter | `error` | 359 | +| parameter | `kind` | 359 | +| parameter | `language` | 359 | +| parameter | `project` | 359 | +| parameter | `result` | 359 | +| parameter | `self` | 359 | +| variable | `row` | 362 | +| variable | `key` | 376 | +| function | `save(self)` | 380 | +| parameter | `self` | 380 | +| variable | `record` | 382 | +| variable | `temporary` | 383 | +| variable | `lines` | 386 | +| variable | `row` | 394 | +| variable | `checks` | 395 | +| variable | `lines` | 399 | +| function | `broker_scenarios(evidence)` | 407 | +| parameter | `evidence` | 407 | +| variable | `language` | 409 | +| variable | `baseline` | 410 | +| variable | `kind` | 411 | +| variable | `project` | 414 | +| variable | `result` | 415 | +| variable | `error` | 416 | +| variable | `result` | 418 | +| variable | `error` | 430 | +| function | `anchor_scenarios(evidence)` | 434 | +| parameter | `evidence` | 434 | +| variable | `language` | 436 | +| variable | `missing_line` | 437 | +| variable | `project` | 437 | +| variable | `unused_line` | 437 | +| variable | `result` | 438 | +| variable | `error` | 439 | +| variable | `result` | 441 | +| variable | `item` | 456 | +| variable | `error` | 461 | +| function | `private_scenarios(evidence)` | 465 | +| parameter | `evidence` | 465 | +| variable | `credentials` | 467 | +| variable | `key` | 468 | +| variable | `directory` | 469 | +| variable | `app` | 470 | +| variable | `client` | 475 | +| variable | `jobs` | 483 | +| variable | `language` | 484 | +| variable | `project` | 485 | +| variable | `result` | 486 | +| variable | `error` | 487 | +| variable | `response` | 489 | +| variable | `revision` | 491 | +| variable | `response` | 492 | +| variable | `job` | 494 | +| variable | `deadline` | 495 | +| variable | `response` | 497 | +| variable | `report` | 499 | +| variable | `result` | 504 | +| variable | `exported` | 512 | +| variable | `html` | 517 | +| variable | `error` | 524 | +| variable | `archives` | 528 | +| variable | `response` | 543 | +| variable | `job` | 549 | +| variable | `result_sha256` | 549 | +| variable | `response` | 550 | +| variable | `exported` | 557 | +| function | `provenance(runtime, expected_source, lane)` | 575 | +| parameter | `expected_source` | 575 | +| parameter | `lane` | 575 | +| parameter | `runtime` | 575 | +| variable | `runtime` | 583 | +| variable | `source` | 588 | +| variable | `dirty` | 590 | +| variable | `worker` | 600 | +| variable | `command` | 605 | +| variable | `image` | 606 | +| function | `toolchain_versions()` | 630 | +| variable | `commands` | 632 | +| variable | `program` | 646 | +| variable | `name` | 651 | +| variable | `directory` | 652 | +| variable | `root` | 653 | +| variable | `argv` | 655 | +| variable | `argv` | 657 | +| variable | `code` | 659 | +| variable | `output` | 659 | +| variable | `versions` | 661 | +| variable | `value` | 667 | +| function | `main()` | 675 | +| variable | `parser` | 677 | +| variable | `args` | 682 | +| variable | `evidence` | 683 | +| variable | `language` | 704 | + +## scripts/csharp_coverage_checks.py + +| Kind | Declaration | Line | +|---|---|---| +| variable | `PROGRAM` | 29 | +| function | `exit_project()` | 39 | +| variable | `code` | 48 | +| variable | `expected` | 48 | +| variable | `name` | 48 | +| variable | `value` | 48 | +| function | `collector_fault(mode)` | 62 | +| parameter | `mode` | 62 | +| variable | `project` | 65 | +| variable | `program` | 76 | +| variable | `name` | 107 | +| variable | `directory` | 108 | +| variable | `root` | 109 | +| variable | `argv` | 111 | +| variable | `argv` | 113 | +| variable | `output` | 115 | +| variable | `status` | 115 | +| variable | `results` | 117 | +| variable | `line` | 119 | +| variable | `observations` | 122 | +| variable | `line` | 124 | +| variable | `observed` | 128 | +| variable | `result` | 128 | +| function | `validate_collector_fault(record)` | 137 | +| parameter | `record` | 137 | +| variable | `mode` | 139 | +| variable | `observed` | 139 | +| variable | `result` | 139 | +| function | `checks(record, output)` | 172 | +| parameter | `output` | 172 | +| parameter | `record` | 172 | +| function | `save()` | 175 | +| variable | `temporary` | 177 | +| variable | `project` | 181 | +| variable | `result` | 182 | +| variable | `case` | 188 | +| variable | `changed` | 192 | +| variable | `unexpected` | 194 | +| variable | `case` | 203 | +| variable | `fault` | 222 | +| variable | `mode` | 236 | +| function | `main()` | 244 | +| variable | `parser` | 246 | +| variable | `args` | 249 | +| variable | `root` | 250 | +| variable | `identity` | 251 | +| variable | `bridge` | 252 | +| variable | `record` | 253 | +| variable | `output` | 260 | ## scripts/draft_lookup.py @@ -2725,29 +2884,30 @@ Generated declaration locations. Python uses AST; JS/TS uses declaration-line ma | variable | `acceptedPackage` | 57 | | variable | `resources` | 64 | | variable | `file` | 65 | -| variable | `metadata` | 98 | -| variable | `page` | 118 | -| variable | `accountStartedAt` | 121 | -| variable | `accountRoutes` | 122 | -| variable | `route` | 124 | -| variable | `route` | 136 | -| variable | `browserErrors` | 148 | -| variable | `health` | 152 | -| variable | `state` | 160 | -| variable | `directory` | 191 | -| variable | `serviceLog` | 194 | -| variable | `statuses` | 195 | -| variable | `lifecycle` | 260 | -| variable | `records` | 261 | -| variable | `launcher` | 268 | -| variable | `line` | 271 | -| variable | `service` | 276 | -| variable | `line` | 277 | -| variable | `diagnostic` | 284 | -| variable | `records` | 288 | -| variable | `stage` | 289 | -| variable | `record` | 301 | -| variable | `pid` | 314 | +| variable | `file` | 91 | +| variable | `metadata` | 103 | +| variable | `page` | 123 | +| variable | `accountStartedAt` | 126 | +| variable | `accountRoutes` | 127 | +| variable | `route` | 129 | +| variable | `route` | 141 | +| variable | `browserErrors` | 153 | +| variable | `health` | 157 | +| variable | `state` | 165 | +| variable | `directory` | 196 | +| variable | `serviceLog` | 199 | +| variable | `statuses` | 200 | +| variable | `lifecycle` | 265 | +| variable | `records` | 266 | +| variable | `launcher` | 273 | +| variable | `line` | 276 | +| variable | `service` | 281 | +| variable | `line` | 282 | +| variable | `diagnostic` | 289 | +| variable | `records` | 293 | +| variable | `stage` | 294 | +| variable | `record` | 306 | +| variable | `pid` | 319 | ## tests/electron-readiness.test.cjs @@ -2939,41 +3099,58 @@ Generated declaration locations. Python uses AST; JS/TS uses declaration-line ma | Kind | Declaration | Line | |---|---|---| -| function | `complete_result(project)` | 24 | -| parameter | `project` | 24 | -| variable | `content` | 26 | -| variable | `case` | 42 | -| variable | `case` | 46 | -| variable | `case` | 63 | -| function | `test_practical_matrix_is_complete_and_independent()` | 80 | -| variable | `language` | 84 | -| variable | `project` | 85 | -| variable | `case` | 87 | -| variable | `duplicate` | 91 | -| function | `test_positive_validator_rejects_incomplete_or_fabricated_evidence(defect)` | 124 | -| parameter | `defect` | 124 | -| variable | `project` | 126 | -| variable | `result` | 127 | -| function | `test_exact_compiler_location_and_nonexecution_are_required()` | 178 | -| variable | `project` | 180 | -| variable | `result` | 181 | -| variable | `defect` | 199 | -| variable | `changed` | 200 | -| function | `test_runtime_failure_is_distinct_from_incorrect_oracle()` | 211 | -| variable | `project` | 213 | -| variable | `result` | 214 | -| variable | `wrong` | 236 | -| function | `test_real_execution_guard_refuses_this_pc_before_subprocess(monkeypatch)` | 247 | -| parameter | `monkeypatch` | 247 | -| function | `test_actual_first_hosted_reports_retain_their_real_tool_semantics(language, kind)` | 258 | -| parameter | `kind` | 258 | -| parameter | `language` | 258 | -| variable | `observed` | 262 | -| variable | `item` | 265 | -| variable | `row` | 265 | -| variable | `project` | 266 | -| variable | `changed` | 271 | -| variable | `changed` | 275 | +| function | `complete_result(project)` | 30 | +| parameter | `project` | 30 | +| variable | `content` | 32 | +| variable | `result` | 33 | +| variable | `case` | 48 | +| variable | `case` | 52 | +| variable | `case` | 69 | +| variable | `source_hash` | 85 | +| variable | `file` | 87 | +| variable | `bridge_hash` | 93 | +| variable | `case` | 96 | +| variable | `index` | 96 | +| variable | `record` | 96 | +| variable | `raw` | 97 | +| function | `test_practical_matrix_is_complete_and_independent()` | 119 | +| variable | `language` | 123 | +| variable | `project` | 124 | +| variable | `case` | 126 | +| variable | `duplicate` | 130 | +| function | `test_positive_validator_rejects_incomplete_or_fabricated_evidence(defect)` | 163 | +| parameter | `defect` | 163 | +| variable | `project` | 165 | +| variable | `result` | 166 | +| function | `test_exact_compiler_location_and_nonexecution_are_required()` | 217 | +| variable | `project` | 219 | +| variable | `result` | 220 | +| variable | `defect` | 238 | +| variable | `changed` | 239 | +| function | `test_runtime_failure_is_distinct_from_incorrect_oracle()` | 250 | +| variable | `project` | 252 | +| variable | `result` | 253 | +| variable | `wrong` | 275 | +| function | `test_real_execution_guard_refuses_this_pc_before_subprocess(monkeypatch)` | 286 | +| parameter | `monkeypatch` | 286 | +| function | `test_actual_first_hosted_reports_retain_their_real_tool_semantics(language, kind)` | 297 | +| parameter | `kind` | 297 | +| parameter | `language` | 297 | +| variable | `observed` | 301 | +| variable | `item` | 304 | +| variable | `row` | 304 | +| variable | `project` | 305 | +| variable | `changed` | 310 | +| variable | `changed` | 314 | +| function | `test_current_csharp_rejects_the_historical_partial_allowance()` | 320 | +| variable | `observed` | 322 | +| variable | `item` | 323 | +| variable | `row` | 323 | +| function | `test_current_csharp_requires_separate_source_bound_exit_observation(defect)` | 331 | +| parameter | `defect` | 331 | +| variable | `project` | 333 | +| variable | `result` | 334 | +| variable | `case` | 335 | ## tests/test_completed_offload.py @@ -3065,6 +3242,233 @@ Generated declaration locations. Python uses AST; JS/TS uses declaration-line ma | variable | `path` | 215 | | variable | `row` | 216 | +## tests/test_csharp_collector_controls.py + +| Kind | Declaration | Line | +|---|---|---| +| function | `record(mode)` | 18 | +| parameter | `mode` | 18 | +| variable | `observed` | 20 | +| function | `test_observed_fault_is_required_and_must_be_blocked(mode)` | 39 | +| parameter | `mode` | 39 | +| variable | `actual` | 41 | +| variable | `changed` | 43 | +| variable | `changed` | 47 | +| function | `test_write_failure_must_follow_valid_successful_target_observation()` | 58 | +| variable | `actual` | 60 | + +## tests/test_csharp_coverage_bridge.py + +| Kind | Declaration | Line | +|---|---|---| +| variable | `SOURCE` | 30 | +| function | `analysis(monkeypatch, tmp_path)` | 34 | +| parameter | `monkeypatch` | 34 | +| parameter | `tmp_path` | 34 | +| variable | `root` | 36 | +| variable | `target` | 37 | +| function | `request(analysis, body, case_index)` | 46 | +| parameter | `analysis` | 46 | +| parameter | `body` | 46 | +| parameter | `case_index` | 46 | +| function | `stub_observation(monkeypatch, analysis, body, exit_code)` | 51 | +| parameter | `analysis` | 51 | +| parameter | `body` | 51 | +| parameter | `exit_code` | 51 | +| parameter | `monkeypatch` | 51 | +| variable | `path` | 53 | +| variable | `value` | 53 | +| parameter | `args` | 55 | +| parameter | `kwargs` | 55 | +| function | `test_request_binds_full_unicode_byte_input_and_never_overwrites(analysis)` | 61 | +| parameter | `analysis` | 61 | +| variable | `body` | 63 | +| variable | `path` | 64 | +| variable | `value` | 64 | +| variable | `another` | 84 | +| variable | `different` | 84 | +| function | `test_request_invalid_context_rejected_before_process(analysis, kind)` | 92 | +| parameter | `analysis` | 92 | +| parameter | `kind` | 92 | +| variable | `body` | 94 | +| variable | `index` | 94 | +| variable | `source` | 94 | +| variable | `source` | 96 | +| variable | `source` | 98 | +| variable | `index` | 100 | +| variable | `index` | 102 | +| variable | `index` | 104 | +| variable | `body` | 106 | +| function | `test_forced_request_name_collision_preserves_original(monkeypatch, analysis)` | 112 | +| parameter | `analysis` | 112 | +| parameter | `monkeypatch` | 112 | +| parameter | `count` | 114 | +| variable | `_value` | 115 | +| variable | `path` | 115 | +| variable | `original` | 116 | +| function | `test_actual_inert_child_exit_and_binary_stdin_are_preserved(analysis, exit_code)` | 123 | +| parameter | `analysis` | 123 | +| parameter | `exit_code` | 123 | +| variable | `body` | 125 | +| variable | `witness` | 126 | +| variable | `path` | 133 | +| variable | `value` | 133 | +| variable | `observed` | 136 | +| variable | `key` | 140 | +| variable | `key` | 141 | +| variable | `val` | 141 | +| function | `test_actual_inert_signal_exit_is_not_normalized_to_collector_exit(analysis)` | 146 | +| parameter | `analysis` | 146 | +| variable | `path` | 149 | +| variable | `value` | 149 | +| function | `test_receipt_is_absent_until_process_wait_returns_and_argv_has_no_shell(monkeypatch, analysis)` | 154 | +| parameter | `analysis` | 154 | +| parameter | `monkeypatch` | 154 | +| variable | `body` | 156 | +| variable | `path` | 157 | +| variable | `value` | 157 | +| variable | `receipt` | 158 | +| variable | `calls` | 159 | +| function | `run(argv)` | 161 | +| parameter | `argv` | 161 | +| parameter | `kwargs` | 161 | +| function | `test_changed_stdin_never_starts_target(monkeypatch, analysis)` | 175 | +| parameter | `analysis` | 175 | +| parameter | `monkeypatch` | 175 | +| variable | `path` | 177 | +| variable | `value` | 177 | +| parameter | `args` | 179 | +| parameter | `kwargs` | 179 | +| function | `test_request_changed_during_target_cannot_publish_completion(monkeypatch, analysis)` | 186 | +| parameter | `analysis` | 186 | +| parameter | `monkeypatch` | 186 | +| variable | `path` | 188 | +| variable | `value` | 188 | +| function | `run(argv)` | 190 | +| parameter | `argv` | 190 | +| parameter | `kwargs` | 190 | +| variable | `altered` | 191 | +| function | `test_missing_observation_never_looks_like_zero_exit(analysis)` | 201 | +| parameter | `analysis` | 201 | +| variable | `path` | 203 | +| variable | `value` | 203 | +| function | `test_malformed_request_cannot_launch_the_target(monkeypatch, analysis, defect)` | 221 | +| parameter | `analysis` | 221 | +| parameter | `defect` | 221 | +| parameter | `monkeypatch` | 221 | +| variable | `path` | 223 | +| variable | `value` | 223 | +| variable | `altered` | 224 | +| variable | `changes` | 225 | +| variable | `changed` | 233 | +| variable | `field` | 233 | +| variable | `content` | 235 | +| variable | `content` | 237 | +| variable | `content` | 239 | +| variable | `content` | 241 | +| parameter | `args` | 244 | +| parameter | `kwargs` | 244 | +| function | `test_existing_receipt_cannot_be_replaced_or_execute_again(monkeypatch, analysis)` | 251 | +| parameter | `analysis` | 251 | +| parameter | `monkeypatch` | 251 | +| variable | `path` | 253 | +| variable | `receipt` | 253 | +| variable | `value` | 253 | +| variable | `original` | 254 | +| parameter | `args` | 256 | +| parameter | `kwargs` | 256 | +| function | `test_observation_tamper_and_malformed_bytes_rejected(monkeypatch, analysis, defect)` | 283 | +| parameter | `analysis` | 283 | +| parameter | `defect` | 283 | +| parameter | `monkeypatch` | 283 | +| variable | `path` | 285 | +| variable | `receipt` | 285 | +| variable | `value` | 285 | +| variable | `observed` | 286 | +| variable | `changes` | 287 | +| variable | `changed` | 301 | +| variable | `key` | 301 | +| variable | `body` | 303 | +| variable | `body` | 305 | +| variable | `body` | 307 | +| variable | `body` | 309 | +| variable | `body` | 311 | +| function | `test_reader_rejects_changed_expected_request(monkeypatch, analysis)` | 317 | +| parameter | `analysis` | 317 | +| parameter | `monkeypatch` | 317 | +| variable | `_receipt` | 319 | +| variable | `path` | 319 | +| variable | `value` | 319 | +| variable | `changed` | 320 | +| function | `test_aliasing_paths_are_not_accepted_as_owned_regular_receipts(monkeypatch, analysis, kind)` | 327 | +| parameter | `analysis` | 327 | +| parameter | `kind` | 327 | +| parameter | `monkeypatch` | 327 | +| variable | `path` | 329 | +| variable | `receipt` | 329 | +| variable | `value` | 329 | +| variable | `selected` | 330 | +| variable | `alias` | 331 | +| variable | `original` | 335 | + +## tests/test_csharp_measurement.py + +| Kind | Declaration | Line | +|---|---|---| +| function | `measurement(monkeypatch, tmp_path, actual_exit, defect)` | 31 | +| parameter | `actual_exit` | 31 | +| parameter | `defect` | 31 | +| parameter | `monkeypatch` | 31 | +| parameter | `tmp_path` | 31 | +| variable | `project` | 33 | +| variable | `work` | 38 | +| variable | `root` | 40 | +| parameter | `_args` | 44 | +| variable | `observed` | 46 | +| variable | `target_exits` | 46 | +| variable | `launched` | 47 | +| function | `target(argv)` | 49 | +| parameter | `argv` | 49 | +| parameter | `kwargs` | 49 | +| function | `execute(argv, stdin, timeout, env)` | 57 | +| parameter | `argv` | 57 | +| parameter | `env` | 57 | +| parameter | `stdin` | 57 | +| parameter | `timeout` | 57 | +| variable | `result` | 58 | +| variable | `destination` | 61 | +| variable | `target_args` | 68 | +| variable | `request_path` | 70 | +| variable | `request` | 71 | +| variable | `index` | 72 | +| variable | `receipt` | 77 | +| variable | `content` | 79 | +| variable | `output` | 82 | +| variable | `coverage` | 105 | +| variable | `unused` | 105 | +| function | `test_real_bridge_observation_keeps_target_nonzero_separate_from_collector(monkeypatch, tmp_path, actual_exit, expected_status)` | 111 | +| parameter | `actual_exit` | 112 | +| parameter | `expected_status` | 112 | +| parameter | `monkeypatch` | 112 | +| parameter | `tmp_path` | 112 | +| variable | `coverage` | 115 | +| variable | `observed` | 115 | +| variable | `project` | 115 | +| variable | `target_exits` | 115 | +| variable | `row` | 119 | +| variable | `row` | 120 | +| variable | `file` | 122 | +| variable | `case` | 124 | +| variable | `index` | 124 | +| function | `test_native_measurement_cannot_promote_bad_collection_to_coverage(monkeypatch, tmp_path, defect)` | 147 | +| parameter | `defect` | 147 | +| parameter | `monkeypatch` | 147 | +| parameter | `tmp_path` | 147 | +| variable | `_project` | 149 | +| variable | `coverage` | 149 | +| variable | `observed` | 149 | +| variable | `target_exits` | 149 | + ## tests/test_draft_lookup.py | Kind | Declaration | Line | @@ -4189,14 +4593,15 @@ Generated declaration locations. Python uses AST; JS/TS uses declaration-line ma | variable | `result` | 303 | | variable | `identifier` | 311 | | variable | `result` | 312 | -| function | `validate(result, project)` | 331 | +| function | `validate(result, project, historical_source)` | 331 | +| parameter | `historical_source` | 331 | | parameter | `project` | 331 | | parameter | `result` | 331 | -| variable | `evidence` | 337 | -| variable | `service` | 338 | -| variable | `private` | 339 | -| variable | `row` | 341 | -| variable | `text` | 342 | +| variable | `evidence` | 338 | +| variable | `service` | 339 | +| variable | `private` | 340 | +| variable | `row` | 342 | +| variable | `text` | 343 | ## tests/test_sanitizer_probe.py diff --git a/docs/compiler-execution.md b/docs/compiler-execution.md index 33493c2..13419f2 100644 --- a/docs/compiler-execution.md +++ b/docs/compiler-execution.md @@ -97,9 +97,9 @@ These are flat-project demonstrations, not production domain algorithms or an empirical Big-O measurement. Timings include process startup; small changes and cross-language rankings are not reliable performance comparisons. -### Tool-specific coverage completion +### Historical 0.4.2 coverage completion -The C# malformed-input case correctly returns exit 2 in the ordinary execution. +The historical 0.4.2 C# malformed-input case correctly returns exit 2 in ordinary execution. Coverlet 8.0.1 wraps that expected nonzero target exit as collector exit 1. The application conservatively retains `coverage.status: partial`, despite a valid source-bound report. Acceptance requires all six ordinary case outputs/exits, @@ -118,3 +118,8 @@ source line; location bounds use the same newline convention as the app/editor. The [observed report snapshots](../tests/fixtures/compiler-reports-042.json) retain these actual first-run results, including their original failed-acceptance labels; replay unit tests are not a new compiler run. + +Current 0.4.3 workers use a [separate exact target-exit bridge](csharp-coverage.md) +and must report measured completion for approved nonzero exits. The old partial +allowance is available only to the independently pinned 0.4.2 baseline verifier; +it cannot make a current worker pass acceptance. diff --git a/docs/csharp-coverage.md b/docs/csharp-coverage.md new file mode 100644 index 0000000..72a054d --- /dev/null +++ b/docs/csharp-coverage.md @@ -0,0 +1,92 @@ +# Exact C# coverage exits — 0.4.3 + +By Jason Stys. See the [dated validation report](reports/csharp-coverage-2026-11-10.md) +for observed hosted results, rather than treating this design as test evidence. + +## Correct behavior + +An approved C# CLI rejection such as exit 2 is not a failed coverage collector. +Version 0.4.2 passed the program's real output/exit cases, but Coverlet 8.0.1 +collapsed nonzero target exits to its wrapper failure code. The application +conservatively reported partial coverage. Version 0.4.3 removes that ambiguity +with a trusted worker bridge; it does not guess the exit from collector text or +convert every collector failure into success. + +| Evidence | Meaning | +|---|---| +| `coverage.cases[].exit_code` | Compatibility field containing the actual instrumented .NET process return | +| `target_exit_code` | The same independently observed target return, explicitly named | +| `expected_exit_code` | Developer-approved oracle; never rewritten by collection | +| `collector_exit_code` | Coverlet's own process result; must be zero for a completed collection | +| `bridge` | Fresh case/source/input/helper binding and the actual child-process return | + +All approved instrumented returns must equal their oracles before coverage is +`measured`. A valid target returning 7 when 2 was expected stays a failed test +with partial coverage. Collector errors/timeouts, missing or mismatched records +and missing/invalid XML remain blocked. A genuine unexpected exception is never +relabeled as a successful approved run. + +## Collection flow + +1. Build the debug C# assembly and portable PDB normally, offline. +2. Create a fresh, bounded request in the existing job-private analysis directory. + Bind a nonce, case index, SHA-256 of all supplied files and exact stdin bytes. +3. Coverlet instruments that assembly and invokes fixed isolated Python under + `/opt`. The bridge can launch only the fixed .NET executable and this job's + fixed assembly, using argv without a shell. +4. The bridge bounds and verifies stdin, forwards the exact bytes, waits for the + actual .NET child and exclusively publishes its observed exit. It returns zero + to Coverlet only after that observation is committed. It does not detach from + the process group or introduce another execution timeout policy. +5. The worker independently requires collector success, complete output, a fresh + strict record matching this request and valid actual source-bound coverage XML. + It then compares the observed target exit against the approved expectation. + +The request/record has a closed schema with exact types; booleans are not integer +exit codes. Size budgets, duplicate/nonfinite JSON rejection, same-descriptor +regular-file checks, no-follow/nonblocking opens and one-link files reject stale, +aliased or malformed protocol data. Publication uses a flushed temporary file +and exclusive atomic hardlink, without overwriting a prior record. Input is +bounded at 32,768 bytes, preserving the API's 8,192-character UTF-8 allowance. + +The worker retains its existing no-network, read-only-root, non-root UID, +capability, resource and process-group controls. The outer worker timeout kills +the owned collector/bridge/target group; an incomplete observation cannot authorize +measured coverage. No provider credentials or Docker socket are passed to target code. + +Target code and instrumentation share a container UID and can interfere with +job-local files, just as with existing coverage outputs. The bindings detect stale +or inconsistent observations; they are not hostile-target tamper-proof attestation. +Use a dedicated disposable environment for untrusted hostile code. + +## Versions and installation scope + +The corrected source, worker image and native build identity are 0.4.3; the worker +is `code-flow-worker:0.4.3`. Published 0.4.2 tags/assets, historical snapshots and +the baseline workflow lane remain unchanged. Only a verifier explicitly bound to +the immutable old release source may accept that historical partial collector +behavior; current acceptance requires the new bridge and measured completion. + +This change does not install an application, rebuild local worker images or repair +Docker on a user's PC. A 0.4.3 application/worker build is required to use the fix; +an existing installed 0.4.2 application and its data are not silently upgraded. +Published download availability is determined by [Releases](https://github.com/JasonStys/code-flow-studio/releases), +not by a source-version label or successful CI package build. + +## Tests and files + +| File | Responsibility | +|---|---| +| `runner/csharp_coverage_bridge.py` | Fixed child-process launch, bounded case binding and exclusive observation publication | +| `runner/language_checks.py` | Separate actual target/collector exits, strict record and real XML integration | +| `scripts/csharp_coverage_checks.py` | Hosted real .NET exits 0/1/2/7/255, malformed input, wrong expected exit, exception, parser rejection, post-target report-write failure and real collector timeout | +| `tests/test_csharp_coverage_bridge.py` | Real inert child-process and protocol/filesystem regressions; no local .NET or Docker | +| `tests/test_csharp_measurement.py` | Actual adapter/request/record/XML integration with inert compiler/collector doubles | +| `scripts/compiler_acceptance.py` | Strict current evidence; separately pinned historical 0.4.2 policy | +| `tests/desktop.spec.ts` | Actual packaged bridge presence and exact source-digest comparison | + +The [compiler workflow](../.github/workflows/compiler-execution.yml) builds each +lane's own versioned image. The current lane runs the six explicit exit oracles +and fault injection through the unchanged restricted worker path. The ordinary +realistic C# invoice is also tested through the real private account API and +encrypted report replay. No local Docker execution is needed for validation. diff --git a/docs/handbook.md b/docs/handbook.md index a51f2a9..91d0358 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -2,6 +2,10 @@ By Jason Stys. +Current source is 0.4.3, with the [exact C# target/collector exit correction](csharp-coverage.md) +and [its own validation report](reports/csharp-coverage-2026-11-10.md). This source +change does not replace published 0.4.2 packages or install anything on a user's PC. + Version [0.4.2 is published](https://github.com/JasonStys/code-flow-studio/releases/tag/v0.4.2). Read its [release notes](releases/v0.4.2.md), the [observed draft-verification CI correction](reports/draft-release-visibility-2026-11-10.md) diff --git a/docs/located-diagnostics.md b/docs/located-diagnostics.md index 0d5de9b..bb6e573 100644 --- a/docs/located-diagnostics.md +++ b/docs/located-diagnostics.md @@ -7,7 +7,7 @@ Version 0.3 extends the original Python contract with [native coverage and unuse ## Use it 1. Upload the flat source files or edit them in Workspace. Select **Static review · no execution**. The Evidence view lists original filename, one-based line/column, check code and explanation. This works without Docker or a configured AI provider. -2. Approve independent stdin/stdout cases, save the revision and run checks with the updated isolated worker. Evidence adds compiler/runtime references, native unused candidates and **Uncovered source locations** with a language-specific metric. Rebuild the worker after updating source: `docker build -t code-flow-worker:0.4.2 runner`. This command is for an already working, authorized Docker engine; the application never repairs Docker or falls back to executing targets on the host. +2. Approve independent stdin/stdout cases, save the revision and run checks with the updated isolated worker. Evidence adds compiler/runtime references, native unused candidates and **Uncovered source locations** with a language-specific metric. Rebuild the worker after updating source: `docker build -t code-flow-worker:0.4.3 runner`. This command is for an already working, authorized Docker engine; the application never repairs Docker or falls back to executing targets on the host. 3. Download JSON, HTML or ZIP. HTML now includes a readable location table and per-file missing-line lists before raw evidence. JSON and ZIP retain the same structured fields and source/test snapshots. Check a file's `source_sha256` against that snapshot before applying a finding to another version. ## What is checked diff --git a/docs/polyglot-locations.md b/docs/polyglot-locations.md index a750d11..7a85fc5 100644 --- a/docs/polyglot-locations.md +++ b/docs/polyglot-locations.md @@ -7,7 +7,7 @@ By Jason Stys. Version 0.3 adds native evidence to every existing execution adap Rebuild the reviewed worker on an **already working, authorized** local Docker engine: ```sh -docker build -t code-flow-worker:0.4.2 runner +docker build -t code-flow-worker:0.4.3 runner ``` The new image tag prevents silently reusing the old Python-only coverage worker. No Docker repair, privileged container, host execution fallback, additional network or host filesystem access is introduced. Installed UI packages include the worker sources and locked tool manifest, not an installed Docker engine or container image. diff --git a/docs/private-accounts.md b/docs/private-accounts.md index 98d854c..1ba3efd 100644 --- a/docs/private-accounts.md +++ b/docs/private-accounts.md @@ -42,7 +42,7 @@ Provider keys are input once to the local service, saved only in its encrypted a Review destinations, then opt in to sending project text. Requirements/cases/harness/source/prompt may be sent; binary fixtures are not sent. Cloud providers may store that text or charge fees under their policies. No external calls happen without an explicitly configured provider and consent. Protocol tests use synthetic mocks, not live keys or model-quality claims. Proposals are unverified, must be reviewed, and cannot silently replace approved oracles or claim test evidence. -Compiler jobs require a local Unix/named-pipe engine and the reviewed `code-flow-worker:0.4.2` image. Remote Docker contexts are blocked in account mode; targets get no network, broad host mounts, account database, provider settings or Docker socket. No automatic Docker installation/repair and no direct-host execution fallback exist. Training remains a separate optional, rights-checked ML workflow, not bundled in the end-user runtime. Version 0.3 adds [native coverage/unused-code evidence](polyglot-locations.md); static Python review remains available without Docker. +Compiler jobs require a local Unix/named-pipe engine and the reviewed `code-flow-worker:0.4.3` image. Remote Docker contexts are blocked in account mode; targets get no network, broad host mounts, account database, provider settings or Docker socket. No automatic Docker installation/repair and no direct-host execution fallback exist. Training remains a separate optional, rights-checked ML workflow, not bundled in the end-user runtime. Version 0.3 adds [native coverage/unused-code evidence](polyglot-locations.md); static Python review remains available without Docker. Private readiness is refreshed in the background every thirty seconds, without blocking source saving. The UI polls that cached badge every five seconds; it never renews the human-idle clock. Each actual execution rechecks and pins the engine, so a stale ready badge cannot authorize a host fallback. diff --git a/docs/releases/v0.4.3.md b/docs/releases/v0.4.3.md new file mode 100644 index 0000000..3ca9704 --- /dev/null +++ b/docs/releases/v0.4.3.md @@ -0,0 +1,27 @@ +# Code Flow Studio 0.4.3 — C# coverage exit preservation + +By Jason Stys. This source update corrects C# instrumented coverage. Hosted +compiler, security and native-package checks passed for the reviewed source; see +the [dated validation report](../reports/csharp-coverage-2026-11-10.md) for exact +sources, runs and measurement limits. No 0.4.3 download has been published. + +The new worker retains a C# target's actual exit status separately +from Coverlet's collector status. Normal compilation and approved-case results +remain independent of instrumented coverage. Current validation must distinguish +expected exits 0, 1, 2, 7 and 255 from an unexpected application fault, including +execution through the authenticated API and encrypted-history recovery. + +Product, runtime, UI, package and worker-image identities advance to 0.4.3. New +packages must include the reviewed `csharp_coverage_bridge.py` resource and match +its source digest during actual packaged-application acceptance. Metadata schema +1 keeps its existing Node-analyzer-only `worker_sha256` field; the exact source +commit identifies the complete source tree. + +The published 0.4.2 downloads, their source, captured compiler observations and +known partial Coverlet coverage remain historical evidence. This change does +not replace those assets, install an update on the owner's PC, repair Docker, +alter existing accounts, or permit target execution outside an isolated worker. + +Any eventual fresh downloads remain subject to checksums, source-bound release +gates and actual platform acceptance. Launching an extracted package is distinct +from testing an operating-system installation or upgrade transaction. diff --git a/docs/reports/csharp-coverage-2026-11-10.json b/docs/reports/csharp-coverage-2026-11-10.json new file mode 100644 index 0000000..09a53cd --- /dev/null +++ b/docs/reports/csharp-coverage-2026-11-10.json @@ -0,0 +1,465 @@ +{ + "schema": 1, + "recordedUtc": "2026-11-10T19:31:57.953212+00:00", + "version": "0.4.3", + "reviewedHead": "4bf014904b7b032887cb720b169af2d72874ff45", + "runtimeSource": "f6d313754c9b9db2f8fdb6a4452ffc48f2286c14", + "observedRuns": [ + { + "databaseId": 42883764689, + "workflowName": "Compiler execution acceptance", + "event": "pull_request", + "headSha": "4bf014904b7b032887cb720b169af2d72874ff45", + "conclusion": "success", + "url": "https://github.com/JasonStys/code-flow-studio/actions/runs/42883764689", + "jobs": [ + { + "name": "Realistic compilers (release-0.4.2)", + "conclusion": "success" + }, + { + "name": "Realistic compilers (current)", + "conclusion": "success" + } + ] + }, + { + "databaseId": 42883753475, + "workflowName": "Compiler execution acceptance", + "event": "push", + "headSha": "4bf014904b7b032887cb720b169af2d72874ff45", + "conclusion": "success", + "url": "https://github.com/JasonStys/code-flow-studio/actions/runs/42883753475", + "jobs": [ + { + "name": "Realistic compilers (current)", + "conclusion": "success" + }, + { + "name": "Realistic compilers (release-0.4.2)", + "conclusion": "success" + } + ] + }, + { + "databaseId": 42883765065, + "workflowName": "Validate", + "event": "pull_request", + "headSha": "4bf014904b7b032887cb720b169af2d72874ff45", + "conclusion": "success", + "url": "https://github.com/JasonStys/code-flow-studio/actions/runs/42883765065", + "jobs": [ + { + "name": "controller", + "conclusion": "success" + }, + { + "name": "training-smoke", + "conclusion": "success" + }, + { + "name": "isolated-workers", + "conclusion": "success" + } + ] + }, + { + "databaseId": 42883753364, + "workflowName": "Validate", + "event": "push", + "headSha": "4bf014904b7b032887cb720b169af2d72874ff45", + "conclusion": "success", + "url": "https://github.com/JasonStys/code-flow-studio/actions/runs/42883753364", + "jobs": [ + { + "name": "controller", + "conclusion": "success" + }, + { + "name": "training-smoke", + "conclusion": "success" + }, + { + "name": "isolated-workers", + "conclusion": "success" + } + ] + }, + { + "databaseId": 42883753311, + "workflowName": "Desktop packages", + "event": "push", + "headSha": "4bf014904b7b032887cb720b169af2d72874ff45", + "conclusion": "success", + "url": "https://github.com/JasonStys/code-flow-studio/actions/runs/42883753311", + "jobs": [ + { + "name": "package (macos-latest)", + "conclusion": "success" + }, + { + "name": "package (ubuntu-latest)", + "conclusion": "success" + }, + { + "name": "package (windows-latest)", + "conclusion": "success" + } + ] + }, + { + "databaseId": 42883764587, + "workflowName": "CodeQL", + "event": "pull_request", + "headSha": "4bf014904b7b032887cb720b169af2d72874ff45", + "conclusion": "success", + "url": "https://github.com/JasonStys/code-flow-studio/actions/runs/42883764587", + "jobs": [ + { + "name": "Analyze / javascript-typescript", + "conclusion": "success" + }, + { + "name": "Analyze / actions", + "conclusion": "success" + }, + { + "name": "Analyze / python", + "conclusion": "success" + } + ] + } + ], + "bridgeSha256": "64e1d4e2dfca531b58e595945e937b41de1334eaba71b729e85794f8ff4fcc41", + "exactExitCases": [ + { + "name": "success-zero", + "expected_exit_code": 0, + "exit_code": 0, + "target_exit_code": 0, + "collector_exit_code": 0 + }, + { + "name": "expected-one", + "expected_exit_code": 1, + "exit_code": 1, + "target_exit_code": 1, + "collector_exit_code": 0 + }, + { + "name": "expected-two", + "expected_exit_code": 2, + "exit_code": 2, + "target_exit_code": 2, + "collector_exit_code": 0 + }, + { + "name": "expected-seven", + "expected_exit_code": 7, + "exit_code": 7, + "target_exit_code": 7, + "collector_exit_code": 0 + }, + { + "name": "expected-255", + "expected_exit_code": 255, + "exit_code": 255, + "target_exit_code": 255, + "collector_exit_code": 0 + }, + { + "name": "malformed-input", + "expected_exit_code": 2, + "exit_code": 2, + "target_exit_code": 2, + "collector_exit_code": 0 + } + ], + "coverageStatus": "measured", + "collectorControls": { + "collector_invalid-option": { + "actual_collector": { + "exit_code": 0, + "timeout": false, + "option_rejected": true, + "valid_target_observation": false + }, + "coverage_status": "blocked" + }, + "collector_unwritable-output": { + "actual_collector": { + "exit_code": 101, + "timeout": false, + "option_rejected": false, + "valid_target_observation": true, + "target_exit_code": 0 + }, + "coverage_status": "blocked" + }, + "collector_timeout": { + "actual_collector": { + "exit_code": -9, + "timeout": true, + "option_rejected": false, + "valid_target_observation": false + }, + "coverage_status": "blocked" + } + }, + "unexpectedExit": { + "expected": 2, + "actual": 7, + "collector": 0, + "coverage": "partial", + "failed_cases": 1 + }, + "unexpectedException": { + "target_exit": -6, + "coverage": "partial", + "failed_cases": 1 + }, + "sourceLanes": [ + { + "provenance": { + "lane": "release-0.4.2", + "runtime_source": "eb9b1cd0c3e3fd1e70e397edeffb059073464f74", + "runtime_version": "0.4.2", + "worker_node_sha256": "750b665e545b7cf0806d4c493db86174f7f4e7ab2218aa239718e652ccdce158", + "worker_image_id": "sha256:b782cd6016d955ea46dee3a7733995f86325e8fdcb81e4e2fc9d330eb701adba", + "validation_source": "f6d313754c9b9db2f8fdb6a4452ffc48f2286c14", + "run_id": "42883764689", + "run_attempt": "1", + "host": "GitHub-hosted Linux", + "toolchain_caveat": "The image was freshly built from the identified source. Mutable upstream base tags/APT packages mean it is not asserted identical to a historical image.", + "isolation": { + "network": "none", + "user": "65534:65534", + "read_only": true, + "memory_mib": 1024, + "cpus": 2, + "pids": 128 + }, + "toolchain": { + "python": "Python 3.12.12", + "javascript": "v24.22.2", + "typescript": "Version 5.9.3", + "cpp": "g++ (Debian 12.2.0-14+deb12u1) 12.2.0", + "c": "gcc (Debian 12.2.0-14+deb12u1) 12.2.0", + "java": "javac 17.0.21", + "csharp": "8.0.428", + "rust": "rustc 1.99.1 (58cf8f9c3 2026-10-13)", + "php": "PHP 8.2.35 (cli) (built: Oct 21 2026 06:29:51) (NTS)", + "bash": "GNU bash, version 5.2.15(1)-release (x86_64-pc-linux-gnu)", + "sql": "3.40.1 2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f3alt1", + "assembly": "GNU assembler (GNU Binutils for Debian) 2.40" + }, + "private_history": { + "login_after_logout": "passed", + "sealed_and_reopened_reports": 12, + "plaintext_removed": true + }, + "acceptance": "passed" + }, + "scenarios": 70, + "cases_per_language": 6, + "csharp_direct_coverage": "partial", + "csharp_private_coverage": "partial", + "all_positive_coverage": { + "python": "measured", + "javascript": "measured", + "typescript": "measured", + "cpp": "measured", + "c": "measured", + "java": "measured", + "csharp": "partial", + "rust": "measured", + "php": "measured", + "bash": "measured", + "sql": "measured", + "assembly": "measured" + } + }, + { + "provenance": { + "lane": "current", + "runtime_source": "f6d313754c9b9db2f8fdb6a4452ffc48f2286c14", + "runtime_version": "0.4.3", + "worker_node_sha256": "750b665e545b7cf0806d4c493db86174f7f4e7ab2218aa239718e652ccdce158", + "worker_image_id": "sha256:9d3d6754da0c2b36e159e2919fb17c564ce975cfebfc100ed0afe563812cbeac", + "validation_source": "f6d313754c9b9db2f8fdb6a4452ffc48f2286c14", + "run_id": "42883764689", + "run_attempt": "1", + "host": "GitHub-hosted Linux", + "toolchain_caveat": "The image was freshly built from the identified source. Mutable upstream base tags/APT packages mean it is not asserted identical to a historical image.", + "isolation": { + "network": "none", + "user": "65534:65534", + "read_only": true, + "memory_mib": 1024, + "cpus": 2, + "pids": 128 + }, + "toolchain": { + "python": "Python 3.12.12", + "javascript": "v24.22.2", + "typescript": "Version 5.9.3", + "cpp": "g++ (Debian 12.2.0-14+deb12u1) 12.2.0", + "c": "gcc (Debian 12.2.0-14+deb12u1) 12.2.0", + "java": "javac 17.0.21", + "csharp": "8.0.428", + "rust": "rustc 1.99.1 (58cf8f9c3 2026-10-13)", + "php": "PHP 8.2.35 (cli) (built: Oct 21 2026 06:29:51) (NTS)", + "bash": "GNU bash, version 5.2.15(1)-release (x86_64-pc-linux-gnu)", + "sql": "3.40.1 2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f3alt1", + "assembly": "GNU assembler (GNU Binutils for Debian) 2.40" + }, + "private_history": { + "login_after_logout": "passed", + "sealed_and_reopened_reports": 12, + "plaintext_removed": true + }, + "acceptance": "passed" + }, + "scenarios": 70, + "cases_per_language": 6, + "csharp_direct_coverage": "measured", + "csharp_private_coverage": "measured", + "all_positive_coverage": { + "python": "measured", + "javascript": "measured", + "typescript": "measured", + "cpp": "measured", + "c": "measured", + "java": "measured", + "csharp": "measured", + "rust": "measured", + "php": "measured", + "bash": "measured", + "sql": "measured", + "assembly": "measured" + } + } + ], + "historicalPublishedService": { + "approved_cases": 72, + "languages": [ + { + "approved_cases": 6, + "language": "python", + "result_sha256": "90e6f31225ee32c8c4ba43f198ec72ab9f74d5397be82355b335aeabfcb75b5f", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "javascript", + "result_sha256": "d374e3ef860738082fbc383c0fb87a9eacc7a8d75a12f6f632fa2d3f1b642be6", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "typescript", + "result_sha256": "c04ad0b114ae4aa5f7478a6ea09af4a4b93c5e0ee2b2bbfd15d9d8102d8a3b2d", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "cpp", + "result_sha256": "0205ac23a64579311467a41927a1e5c2ac75c945d028f813561cd82aceddbc44", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "c", + "result_sha256": "0cd5b4932c9223b740504bc705abd5f5f69c9241e096995ff97ffafb6b003d8a", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "java", + "result_sha256": "5de4cfc27a42905ccbd1678902515cdf8f01f62ec1e10495d707c0ce5a4d4774", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "csharp", + "result_sha256": "f39c5aab74a5af0703f773861c8dd30a972ab879643efcddc31742bb45d87c05", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "rust", + "result_sha256": "13abe24615d899ddf110f06e5a255a4ff38f3c394bb5ddcdc1025a0fe7164ae9", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "php", + "result_sha256": "3427e302b73ada624e0474aedd9bfbf21f011ee5b08b4c66c7d6558c949de057", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "bash", + "result_sha256": "ca3725cbc05ec07ce972884ac074dd5e13b8e6cebb82268061b6474930dce31f", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "sql", + "result_sha256": "d20066c58c141bb11eae6ebd3afa0f945966b81a1c744c81fd7af5c2e9e10c7b", + "status": "passed" + }, + { + "approved_cases": 6, + "language": "assembly", + "result_sha256": "fc162eec36afa82b68c86a89758ef3c967338db7598c6ffda6c1122313b6733b", + "status": "passed" + } + ], + "payload_bytes": 188603375, + "payload_sha256": "1e75085bae4ab2611ead61a513b60f444941495608840cf33875d940cf963ebe", + "release_id": 440504623, + "released_source": "eb9b1cd0c3e3fd1e70e397edeffb059073464f74", + "restored_reports": 12, + "schema": 1, + "scope": "Published Linux service with a fresh source-bound0.4.2 hosted Docker worker; not a local PC compiler run or historical image-byte proof", + "stage": "complete", + "status": "passed", + "verifier_source": "f6d313754c9b9db2f8fdb6a4452ffc48f2286c14", + "version": "0.4.2", + "worker_sha256": "750b665e545b7cf0806d4c493db86174f7f4e7ab2218aa239718e652ccdce158" + }, + "testResults": { + "hostedTests": 410, + "hostedSubtests": 28, + "controllerTrainingCoveragePercent": 85.01, + "unchangedCoverageFloor": 80, + "localFocusedPassed": 238, + "localWindowsPlatformSkips": 2, + "openPrMergeCodeqlAlerts": 0 + }, + "evidenceHashes": [ + { + "file": "results.json", + "lane": "release-0.4.2", + "sha256": "f0cd91c97f5f475b46817713ca23eee0590698f489ba97ea2e7bcf8b1ddeb44f" + }, + { + "file": "results.json", + "lane": "current", + "sha256": "7ab9116b1dfae4a785fefc744bed69b5811ea5d32fdd5a3b5c1168eff51b98da" + }, + { + "file": "csharp-coverage.json", + "sha256": "691fe8d86ad2f90375e93b09092ca42de48a6b918d9a00a933dbda76ac7e4ed3" + } + ], + "scope": { + "dockerPcChanged": false, + "installedAppChanged": false, + "accountsDataChanged": false, + "releasePublished": false, + "installedVersionRemains": "0.4.2", + "newApplicationWorkerSourceVersion": "0.4.3" + } +} diff --git a/docs/reports/csharp-coverage-2026-11-10.md b/docs/reports/csharp-coverage-2026-11-10.md new file mode 100644 index 0000000..448a14a --- /dev/null +++ b/docs/reports/csharp-coverage-2026-11-10.md @@ -0,0 +1,67 @@ +# C# coverage correction — 10 November 2026 + +By Jason Stys. **The 0.4.3 worker now reports measured coverage for approved nonzero C# exits, retaining the actual target exit separately from the collector.** The correction passed real .NET/Coverlet validation on GitHub-hosted Linux. Docker, installed applications and account data on the owner's PC were unchanged. + +## Observed exact target results + +| Approved case | Expected target | Actual instrumented target | Collector | Coverage | +|---|---|---|---|---| +| success-zero | 0 | 0 | 0 | measured | +| expected-one | 1 | 1 | 0 | measured | +| expected-two | 2 | 2 | 0 | measured | +| expected-seven | 7 | 7 | 0 | measured | +| expected-255 | 255 | 255 | 0 | measured | +| malformed-input | 2 | 2 | 0 | measured | + +The ordinary realistic invoice also passed all six cases with measured coverage, both through the normal broker and a real authenticated private account. All twelve languages passed the ordinary 70-scenario matrix, including exact native unused/uncovered anchors. Twelve complete reports were sealed, logged out/in, reopened and compared with exported JSON. + +## Genuine failures still fail + +| Controlled fault | Actual collector | Independent target observation | Coverage result | +|---|---|---|---| +| invalid-option | 0 | none | blocked | +| unwritable-output | 101 | 0 | blocked | +| timeout | -9 / timeout | none | blocked | + +The argument-error control proves that collector exit zero alone is insufficient: no instrumented target ran and no valid observation existed. The report-write control completed a real target returning zero before the collector failed writing to the unchanged read-only `/opt` directory. The timeout used the existing process-group deadline and produced no successful observation. No sandbox permissions were widened. + +An actual target return 7 with approved expectation 2 remained a failed ordinary case with partial coverage; the collector itself returned 0. An authored unhandled exception remained a failed case/partial coverage. The fix does not force unsuccessful programs or broken collectors to appear measured. + +## Validation and source identity + +| Check | Observed result | Run | +|---|---|---| +| Compiler execution acceptance | 2 jobs passed | [42883764689](https://github.com/JasonStys/code-flow-studio/actions/runs/42883764689) | +| Validate | 3 jobs passed | [42883765065](https://github.com/JasonStys/code-flow-studio/actions/runs/42883765065) | +| Desktop packages | 3 jobs passed | [42883753311](https://github.com/JasonStys/code-flow-studio/actions/runs/42883753311) | +| CodeQL | 3 jobs passed | [42883764587](https://github.com/JasonStys/code-flow-studio/actions/runs/42883764587) | + +- Hosted full suite: **410 tests and 28 subtests passed**; **85.01% controller/training coverage**, unchanged 80% floor. This percentage is not presented as worker-source coverage. +- Local focused suite: **238 passed**, with two explicit Windows platform skips for POSIX signal/symlink capabilities. The complete hosted Linux suite exercised both platform paths. +- Browser, original isolated worker/fuzz checks, real private-worker replay, training, three-OS compiled native package checks and CodeQL passed. The reviewed PR merge ref had zero open alerts. +- Native package acceptance checked that the new helper was present and its exact bytes matched the reviewed source. +- Reviewed head: [`4bf014904b7b032887cb720b169af2d72874ff45`](https://github.com/JasonStys/code-flow-studio/commit/4bf014904b7b032887cb720b169af2d72874ff45); actual current PR runtime/verifier checkout: `f6d313754c9b9db2f8fdb6a4452ffc48f2286c14`. +- Actual bridge SHA-256: `64e1d4e2dfca531b58e595945e937b41de1334eaba71b729e85794f8ff4fcc41`. Source identities, worker image IDs, compiler versions and raw evidence hashes are in the [machine-readable observation](csharp-coverage-2026-11-10.json). + +These are dated observations for the listed sources/runs, not a certification of an unrun later revision. [Current main workflows](https://github.com/JasonStys/code-flow-studio/actions?query=branch%3Amain) are checked separately after merge. + +## Defect and implementation + +Coverlet 8.0.1 intentionally maps a positive target exit to its own failure code; it has no preserve-target-exit switch. The 0.4.2 application correctly kept the resulting mismatch partial. Inferring the actual program exit from wrapper 1 or log text would be wrong. + +The 0.4.3 trusted worker bridge bounds/verifies exact stdin, launches only the fixed .NET executable and this job's assembly, waits for actual completion and exclusively publishes a fresh source/case/input-bound observation before returning success to Coverlet. The worker independently requires collector success, the valid observation and genuine source-bound XML before comparing actual target status with the approved oracle. Invalid/missing/truncated evidence remains blocked. See the [full field and trust-boundary contract](../csharp-coverage.md). + +## First acceptance failure and corrected control + +The [first current-source attempt](https://github.com/JasonStys/code-flow-studio/actions/runs/42881205975) is preserved as failed evidence. The product fix already passed its 70 ordinary scenarios and all six exact-exit cases. Its fault test incorrectly assumed that invalid Coverlet arguments must return nonzero and required a particular error-message word. The [pinned entry point](https://github.com/coverlet-coverage/coverlet/blob/v8.0.1/src/coverlet.console/Program.cs) invokes the argument parser without propagating its return code, so a parser rejection can exit 0 without invoking the target. + +The corrected control records actual fault evidence before assertions, checks parser rejection/no observation independently, and requires blocked coverage even for wrapper 0. The additional actual post-target report-write failure proves that a valid successful target observation cannot hide a broken collector. No product or oracle relaxation was needed for this test correction. + +## Version, installation and measurement limits + +- The fixed source/application/worker identity is **0.4.3**, using `code-flow-worker:0.4.3`. The immutable 0.4.2 baseline, published binaries and old partial records remain unchanged. Its historical lane still reports partial C# coverage, independently of the strict current lane. +- This task changed source, tests and documentation. It did **not** publish a new release, install a 0.4.3 app, rebuild local images, repair Docker or modify existing accounts/data. An installed 0.4.2 app remains 0.4.2; using the fix locally requires a matching 0.4.3 application/worker build. +- Expected nonzero exits now have fully measured instrumentation; that does not mean every possible source branch has been tested or every line has 100% coverage. Unexpected exits and real tool failures retain partial/blocked states. +- Source, bridge and actual worker image identity are recorded. Mutable upstream base/APT tags mean a fresh source-bound image is not claimed identical to historical image bytes. Timing includes process startup and is not an empirical Big-O or cross-language ranking. +- The existing non-root/no-network/read-only/resource controls remain. Same-UID targets can interfere with job files; record bindings detect stale/inconsistent evidence, not malicious-target tamper-proof attestation. +- Only small synthetic JSON evidence has three-day per-artifact retention. No runs/artifacts/caches were deleted and no account retention policy changed. diff --git a/docs/setup.md b/docs/setup.md index 77aeee1..3c970ec 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -8,7 +8,7 @@ Windows: launch the single `Code Flow Studio Setup … .exe` artifact. It instal Linux uses an AppImage; macOS uses a DMG. Native packages are architecture/OS specific. Mobile devices use the responsive browser interface; native Android/iOS packaging is not implemented. -Execution prerequisites are separate from UI installation: Docker must be running, and the trusted `code-flow-worker:0.4.2` image must exist. Build the image from the reviewed `runner` directory. This can download compilers and libraries during trusted image installation; target jobs have no network. If unavailable, the UI remains usable but execution is explicitly blocked. The new tag is required for [polyglot coverage and unused checks](polyglot-locations.md). +Execution prerequisites are separate from UI installation: Docker must be running, and the trusted `code-flow-worker:0.4.3` image must exist. Build the image from the reviewed `runner` directory. This can download compilers and libraries during trusted image installation; target jobs have no network. If unavailable, the UI remains usable but execution is explicitly blocked. The new tag is required for [polyglot coverage and unused checks](polyglot-locations.md). ## Source developers @@ -20,7 +20,7 @@ python -m venv .venv pip install -e '.[dev]' npm ci npm run build -docker build -t code-flow-worker:0.4.2 runner +docker build -t code-flow-worker:0.4.3 runner python -m studio.cli serve --port 8012 ``` diff --git a/package-lock.json b/package-lock.json index e5159bb..5dc42db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-flow-studio", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-flow-studio", - "version": "0.4.2", + "version": "0.4.3", "license": "MIT", "dependencies": { "@monaco-editor/react": "4.7.0", diff --git a/package.json b/package.json index 88b9350..48f7639 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-flow-studio", - "version": "0.4.2", + "version": "0.4.3", "description": "Isolated polyglot testing and evidence-first AI development studio", "author": "Jason Stys", "license": "MIT", diff --git a/pyproject.toml b/pyproject.toml index 1fad231..d5b80f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "code-flow-studio" -version = "0.4.2" +version = "0.4.3" description = "Evidence-first, isolated polyglot testing and AI development workbench" requires-python = ">=3.12" dependencies = ["fastapi==0.142.1", "uvicorn==0.38.0", "httpx==0.28.1", "pydantic==2.13.2", "cryptography==50.0.2", "psutil==7.2.2", "certifi==2026.10.19"] diff --git a/runner/csharp_coverage_bridge.py b/runner/csharp_coverage_bridge.py new file mode 100644 index 0000000..3659a27 --- /dev/null +++ b/runner/csharp_coverage_bridge.py @@ -0,0 +1,304 @@ +# +# Generated definition map; @L is the exact declaration line. +# Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. +# functions: _require(condition, message)@L45; _hash(value)@L51; _hex(value, length)@L56; _directory(path)@L61; _root(root)@L72; _missing(path)@L78; _regular(metadata)@L87; _read(path, maximum)@L95; _json(path)@L123; pairs(items)@L126; nonfinite(_value)@L131; _request(path)@L143; _validate_request(value, suffix)@L154; _publish(path, value)@L172; _bridge_hash()@L191; make_request(root, source_sha256, case_index, stdin_bytes)@L196; read_observation(request_path, request)@L224; observe(request_path, input_stream)@L254; main()@L293 +# variables: ANALYSIS@L35; DOTNET@L36; ASSEMBLY@L37; MAX_INPUT@L38; MAX_JSON@L39; MAX_BRIDGE@L40; REQUEST_FIELDS@L41; OBSERVATION_FIELDS@L42; component@L64; metadata@L65; before@L97; flags@L100; stream@L103; opened@L104; body@L110; after@L111; value@L127; value@L134; match@L147; value@L149; body@L176; temporary@L178; flags@L179; descriptor@L180; stream@L182; suffix@L207; path@L208; value@L209; actual@L226; suffix@L229; value@L231; echoed@L233; name@L233; name@L236; request@L262; receipt@L263; raw@L269; completed@L277; name@L283; value@L283 +# parameters: condition@L45; message@L45; value@L51; length@L56; value@L56; path@L61; root@L72; path@L78; metadata@L87; maximum@L95; path@L95; path@L123; items@L126; _value@L131; path@L143; suffix@L154; value@L154; path@L172; value@L172; case_index@L196; root@L196; source_sha256@L196; stdin_bytes@L196; request@L224; request_path@L224; input_stream@L254; request_path@L254 +# + +"""Jason Stys: observe the instrumented .NET target independently of Coverlet's exit. + +Coverlet runs this trusted, isolated-mode Python file as its fixed target. The +bridge forwards approved stdin, waits for the fixed assembly and exclusively +publishes a small observation before returning success to the collector. A +collector failure remains a separate failure; no wrapper status is translated +into a guessed target exit. Only the restricted Linux worker may run the CLI. + +Request/observation fields bind one fresh case, all-source hash and input bytes. +Targets share the worker UID and can interfere with job files, so these bindings +detect stale/mismatched evidence, not hostile-code forgery or trusted attestation. +No secrets, target output, network access, new process session or host execution +fallback is introduced. Declaration/variable locations: docs/code-index.md. +""" + +import hashlib +import json +import os +import re +import secrets +import stat +import subprocess +import sys +from pathlib import Path +from typing import BinaryIO + +ANALYSIS = Path("/tmp/project/.analysis") # noqa: S108 -- fixed job-private worker tmpfs. +DOTNET = Path("/usr/share/dotnet/dotnet") +ASSEMBLY = ANALYSIS / "csharp" / "App.dll" +MAX_INPUT = 32768 +MAX_JSON = 4096 +MAX_BRIDGE = 128 * 1024 +REQUEST_FIELDS = {"schema", "nonce", "case_index", "source_sha256", "stdin_sha256", "stdin_bytes", "receipt"} +OBSERVATION_FIELDS = (REQUEST_FIELDS - {"receipt"}) | {"target", "target_exit_code", "bridge_sha256"} + + +def _require(condition: bool, message: str) -> None: + """Reject invalid protocol state explicitly, including under optimized Python.""" + if not condition: + raise ValueError(message) + + +def _hash(value: bytes) -> str: + """Hash exact bytes; never normalize source or input text.""" + return hashlib.sha256(value).hexdigest() + + +def _hex(value, length: int) -> bool: + """Accept only lower-case hexadecimal strings of the required exact length.""" + return isinstance(value, str) and re.fullmatch(r"[a-f0-9]{" + str(length) + "}", value) is not None + + +def _directory(path: Path) -> None: + """Require real directory components instead of following a substituted link.""" + _require(path.is_absolute(), "Bridge path must be absolute") + for component in (path, *path.parents): + metadata = component.lstat() + _require( + stat.S_ISDIR(metadata.st_mode) and not component.is_symlink() and not component.is_junction(), + "Bridge directory must not contain links", + ) + + +def _root(root: Path) -> None: + """Allow only the fixed analysis root; callers cannot choose another output tree.""" + _require(isinstance(root, Path) and root == ANALYSIS, "Unexpected bridge root") + _directory(root) + + +def _missing(path: Path) -> None: + """Reject any existing name, including a dangling symlink, before target launch.""" + try: + path.lstat() + except FileNotFoundError: + return + raise ValueError("Bridge output already exists") + + +def _regular(metadata: os.stat_result) -> None: + """Only one-link regular files are eligible for protocol reads.""" + _require( + stat.S_ISREG(metadata.st_mode) and metadata.st_nlink == 1, + "Bridge protocol requires a one-link regular file", + ) + + +def _read(path: Path, maximum: int) -> bytes: + """Read at most the explicit bound through the same checked file descriptor.""" + before = path.lstat() + _regular(before) + _require(not path.is_symlink() and not path.is_junction(), "Bridge file must not be a link") + flags = ( + os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0) + ) + with os.fdopen(os.open(path, flags), "rb") as stream: + opened = os.fstat(stream.fileno()) + _regular(opened) + _require( + os.path.samestat(before, opened) and 0 <= opened.st_size <= maximum, + "Bridge file identity or size changed", + ) + body = stream.read(maximum + 1) + after = os.fstat(stream.fileno()) + _regular(after) + _require( + len(body) <= maximum + and len(body) == opened.st_size == after.st_size + and opened.st_mtime_ns == after.st_mtime_ns + and os.path.samestat(after, path.lstat()), + "Bridge file changed during its bounded read", + ) + return body + + +def _json(path: Path) -> dict: + """Reject duplicate fields, nonfinite constants, non-UTF8 bytes and non-object JSON.""" + + def pairs(items): + value = dict(items) + _require(len(value) == len(items), "Duplicate bridge JSON field") + return value + + def nonfinite(_value): + raise ValueError("Nonfinite bridge JSON value") + + value = json.loads( + _read(path, MAX_JSON).decode("utf-8"), + object_pairs_hook=pairs, + parse_constant=nonfinite, + ) + _require(isinstance(value, dict), "Bridge JSON must be an object") + return value + + +def _request(path: Path) -> dict: + """Validate a fixed-root request filename and every declared context field.""" + _require(isinstance(path, Path), "Bridge request must be a path") + _root(path.parent) + match = re.fullmatch(r"csharp-request-([a-f0-9]{32})\.json", path.name) + _require(match is not None, "Invalid bridge request name") + value = _json(path) + _validate_request(value, match[1]) + return value + + +def _validate_request(value: dict, suffix: str) -> None: + """Require exact schema/types/ranges and the receipt paired with this request.""" + _require(set(value) == REQUEST_FIELDS, "Unexpected bridge request fields") + _require(type(value["schema"]) is int and value["schema"] == 1, "Invalid bridge schema") + _require(_hex(value["nonce"], 64), "Invalid bridge nonce") + _require( + type(value["case_index"]) is int and 0 <= value["case_index"] <= 63, + "Invalid bridge case index", + ) + _require(_hex(value["source_sha256"], 64), "Invalid bridge source hash") + _require(_hex(value["stdin_sha256"], 64), "Invalid bridge stdin hash") + _require( + type(value["stdin_bytes"]) is int and 0 <= value["stdin_bytes"] <= MAX_INPUT, + "Invalid bridge stdin size", + ) + _require(value["receipt"] == f"csharp-exit-{suffix}.json", "Invalid bridge receipt name") + + +def _publish(path: Path, value: dict) -> None: + """Fsync a private temporary file, then publish using an exclusive hard link.""" + _root(path.parent) + _missing(path) + body = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + _require(len(body) <= MAX_JSON, "Bridge JSON exceeds its byte bound") + temporary = path.parent / ("csharp-publish-" + secrets.token_hex(16) + ".tmp") + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0) + descriptor = os.open(temporary, flags, 0o600) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(body) + stream.flush() + os.fsync(stream.fileno()) + os.link(temporary, path, follow_symlinks=False) + finally: + temporary.unlink(missing_ok=True) + + +def _bridge_hash() -> str: + """Bind observations to the actual bounded trusted bridge source, not a claimed digest.""" + return _hash(_read(Path(__file__), MAX_BRIDGE)) + + +def make_request(root: Path, source_sha256: str, case_index: int, stdin_bytes: bytes) -> tuple[Path, dict]: + """Create a fresh case request without executing a target or replacing existing evidence. + + root is exactly ANALYSIS; source_sha256 binds the canonical complete source + list supplied by the worker. case_index is the approved zero-based case. + stdin_bytes are the exact UTF8-encoded input, including all newlines. + """ + _root(root) + _require(_hex(source_sha256, 64), "Invalid bridge source hash") + _require(type(case_index) is int and 0 <= case_index <= 63, "Invalid bridge case index") + _require(type(stdin_bytes) is bytes and len(stdin_bytes) <= MAX_INPUT, "Invalid bridge stdin") + suffix = secrets.token_hex(16) + path = root / f"csharp-request-{suffix}.json" + value = { + "schema": 1, + "nonce": secrets.token_hex(32), + "case_index": case_index, + "source_sha256": source_sha256, + "stdin_sha256": _hash(stdin_bytes), + "stdin_bytes": len(stdin_bytes), + "receipt": f"csharp-exit-{suffix}.json", + } + _validate_request(value, suffix) + _missing(root / value["receipt"]) + _publish(path, value) + return path, value + + +def read_observation(request_path: Path, request: dict) -> dict: + """Require a fresh on-disk request and fully bound observed status from this bridge.""" + actual = _request(request_path) + _require(isinstance(request, dict) and actual == request, "Bridge request context changed") + # Revalidate caller types too: Python equality alone would accept True as 1. + suffix = re.fullmatch(r"csharp-request-([a-f0-9]{32})\.json", request_path.name)[1] + _validate_request(request, suffix) + value = _json(request_path.parent / request["receipt"]) + _require(set(value) == OBSERVATION_FIELDS, "Unexpected bridge observation fields") + echoed = {name: value[name] for name in REQUEST_FIELDS - {"receipt"}} + _validate_request({**echoed, "receipt": request["receipt"]}, suffix) + _require( + echoed == {name: request[name] for name in REQUEST_FIELDS - {"receipt"}}, + "Bridge observation context mismatch", + ) + _require( + type(value["target"]) is str and value["target"] == str(ASSEMBLY), + "Bridge observation target mismatch", + ) + _require( + type(value["target_exit_code"]) is int and -128 <= value["target_exit_code"] <= 255, + "Invalid observed target exit", + ) + _require( + _hex(value["bridge_sha256"], 64) and value["bridge_sha256"] == _bridge_hash(), + "Bridge source hash mismatch", + ) + return value + + +def observe(request_path: Path, input_stream: BinaryIO) -> int: + """Wait for the fixed target and publish its real status before reporting collector success. + + The existing worker owns the outer deadline/process group. This child never + starts a detached session, inherits output/error streams, and forwards exact + already-validated input bytes. Launch/write/protocol errors produce no valid + observation and propagate to the fixed-error CLI. + """ + request = _request(request_path) + receipt = request_path.parent / request["receipt"] + _missing(receipt) + _require(Path(ASSEMBLY) == ANALYSIS / "csharp" / "App.dll", "Unexpected fixed assembly") + _directory(Path(ASSEMBLY).parent) + _regular(Path(ASSEMBLY).lstat()) + _require(not Path(ASSEMBLY).is_symlink(), "Assembly must not be a link") + raw = input_stream.read(MAX_INPUT + 1) + _require( + type(raw) is bytes + and len(raw) <= MAX_INPUT + and len(raw) == request["stdin_bytes"] + and _hash(raw) == request["stdin_sha256"], + "Bridge stdin differs from the approved case", + ) + completed = subprocess.run([str(DOTNET), str(ASSEMBLY)], input=raw, check=False) + _require( + type(completed.returncode) is int and -128 <= completed.returncode <= 255, + "Target completion status is unavailable", + ) + _require(_request(request_path) == request, "Bridge request changed during target execution") + value = {name: request[name] for name in REQUEST_FIELDS - {"receipt"}} + value.update( + target=str(ASSEMBLY), + target_exit_code=completed.returncode, + bridge_sha256=_bridge_hash(), + ) + _publish(receipt, value) + return 0 + + +def main() -> int: + """Accept one fixed-root request path and emit only an authored failure on error.""" + try: + _require(len(sys.argv) == 2, "One bridge request is required") + return observe(Path(sys.argv[1]), sys.stdin.buffer) + except (OSError, ValueError, TypeError, KeyError): + print("C# coverage target observation failed", file=sys.stderr) + return 125 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/runner/language_checks.py b/runner/language_checks.py index 93bf5b7..95e6843 100644 --- a/runner/language_checks.py +++ b/runner/language_checks.py @@ -1,10 +1,10 @@ # # Generated definition map; @L is the exact declaration line. # Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. -# classes: Evidence@L68 -# functions: read_text(path)@L43; read_json(path)@L50; read_xml(path)@L55; __init__(self, project)@L71; name(self, path)@L90; line(self, name, value)@L95; add_lines(self, path, counts)@L100; finding(self, path, line, column, code, message, severity)@L111; coverage(self, tool, metric, cases, expected)@L136; cobertura(evidence, path, case_index)@L190; jacoco(evidence, path)@L216; gcov(evidence, path)@L235; lcov(evidence, text)@L253; shell_lines(node)@L274; assembly_symbols(evidence)@L291; native_checks(project, work, execute, command)@L322; unused_checks(evidence, root, execute)@L340; measure(evidence, root, execute, command)@L585 -# variables: MAX_REPORT@L24; SUFFIXES@L25; text@L59; text@L62; file@L75; name@L82; text@L82; name@L84; text@L84; value@L92; line@L97; name@L102; lines@L105; count@L106; value@L106; line@L107; name@L113; key@L116; files@L138; name@L139; text@L139; counts@L140; covered@L147; value@L147; count@L158; line@L158; count@L159; line@L159; budget@L165; key@L166; record@L167; values@L168; budget@L170; file@L172; measured@L172; complete@L173; case@L175; file@L185; document@L192; roots@L193; source@L193; item@L194; reported@L195; paths@L196; root@L196; candidate@L197; name@L197; path@L197; line@L201; line@L203; condition@L204; file@L218; name@L219; line@L222; line@L230; stream@L237; text@L238; file@L241; name@L242; line@L245; line@L248; branch@L249; index@L249; name@L255; row@L256; name@L258; _@L260; count@L260; line@L260; block@L263; branch@L263; count@L263; line@L263; output@L276; pending@L277; item@L279; position@L282; name@L293; text@L293; definitions@L294; exported@L295; used@L296; line@L297; source@L297; source@L298; label@L304; source@L307; source@L309; column@L311; label@L311; line@L311; evidence@L324; root@L325; unused@L327; coverage@L331; coverage@L333; project@L342; language@L343; entry@L344; output@L345; allowed@L346; result@L347; parse_failed@L348; result@L351; item@L356; parse_failed@L357; tool@L366; result@L368; item@L384; location@L387; tool@L395; result@L397; line@L415; item@L416; code@L417; span@L419; tool@L428; allowed@L430; result@L431; parsed@L448; parse_failed@L449; file@L450; item@L451; tool@L459; allowed@L461; result@L462; parsed@L479; parse_failed@L480; file@L481; item@L482; tool@L484; allowed@L486; result@L487; item@L499; parse_failed@L500; tool@L504; allowed@L506; result@L507; file@L524; item@L525; parse_failed@L526; tool@L534; result@L536; line@L552; match@L553; tool@L558; result@L561; tool@L562; project@L587; entry@L588; language@L588; work@L589; build@L590; metric@L591; tool@L591; run@L592; metric@L594; tool@L594; build@L595; run@L605; metric@L607; tool@L607; build@L608; run@L623; metric@L625; tool@L625; classes@L626; build@L628; run@L629; metric@L638; tool@L638; build@L639; metric@L641; tool@L641; metric@L644; tool@L644; run@L645; metric@L647; tool@L647; build@L648; metric@L660; tool@L660; name@L661; tree@L662; result@L663; line@L670; metric@L672; tool@L672; result@L674; cases@L677; case@L678; index@L678; output@L679; environment@L680; run@L688; run@L690; run@L701; result@L716; lines@L730; name@L730; count@L731; line@L731; trace@L735; match@L736; current@L739; row@L740; current@L742; _@L744; hits@L744; line@L744; output@L747; result@L748; lines@L754; name@L754; note@L757; result@L758; file@L761; profiles@L764; result@L765; output@L778; result@L779; output@L794; result@L795; result@L816; row@L817; match@L818; name@L820; existing@L822 -# parameters: path@L43; path@L50; path@L55; project@L71; self@L71; path@L90; self@L90; name@L95; self@L95; value@L95; counts@L100; path@L100; self@L100; code@L111; column@L111; line@L111; message@L111; path@L111; self@L111; severity@L111; cases@L136; expected@L136; metric@L136; self@L136; tool@L136; case_index@L190; evidence@L190; path@L190; evidence@L216; path@L216; evidence@L235; path@L235; evidence@L253; text@L253; node@L274; evidence@L291; command@L322; execute@L322; project@L322; work@L322; evidence@L340; execute@L340; root@L340; command@L585; evidence@L585; execute@L585; root@L585 +# classes: Evidence@L74 +# functions: read_text(path)@L49; read_json(path)@L56; read_xml(path)@L61; __init__(self, project)@L77; name(self, path)@L96; line(self, name, value)@L101; add_lines(self, path, counts)@L106; finding(self, path, line, column, code, message, severity)@L117; coverage(self, tool, metric, cases, expected)@L142; cobertura(evidence, path, case_index)@L199; jacoco(evidence, path)@L225; gcov(evidence, path)@L244; lcov(evidence, text)@L262; shell_lines(node)@L283; assembly_symbols(evidence)@L300; native_checks(project, work, execute, command)@L331; unused_checks(evidence, root, execute)@L349; measure(evidence, root, execute, command)@L594 +# variables: MAX_REPORT@L30; SUFFIXES@L31; text@L65; text@L68; file@L81; name@L88; text@L88; name@L90; text@L90; value@L98; line@L103; name@L108; lines@L111; count@L112; value@L112; line@L113; name@L119; key@L122; files@L144; name@L145; text@L145; counts@L146; covered@L153; value@L153; count@L164; line@L164; count@L165; line@L165; budget@L171; key@L172; record@L173; values@L174; budget@L176; file@L178; measured@L178; complete@L179; case@L184; file@L194; document@L201; roots@L202; source@L202; item@L203; reported@L204; paths@L205; root@L205; candidate@L206; name@L206; path@L206; line@L210; line@L212; condition@L213; file@L227; name@L228; line@L231; line@L239; stream@L246; text@L247; file@L250; name@L251; line@L254; line@L257; branch@L258; index@L258; name@L264; row@L265; name@L267; _@L269; count@L269; line@L269; block@L272; branch@L272; count@L272; line@L272; output@L285; pending@L286; item@L288; position@L291; name@L302; text@L302; definitions@L303; exported@L304; used@L305; line@L306; source@L306; source@L307; label@L313; source@L316; source@L318; column@L320; label@L320; line@L320; evidence@L333; root@L334; unused@L336; coverage@L340; coverage@L342; project@L351; language@L352; entry@L353; output@L354; allowed@L355; result@L356; parse_failed@L357; result@L360; item@L365; parse_failed@L366; tool@L375; result@L377; item@L393; location@L396; tool@L404; result@L406; line@L424; item@L425; code@L426; span@L428; tool@L437; allowed@L439; result@L440; parsed@L457; parse_failed@L458; file@L459; item@L460; tool@L468; allowed@L470; result@L471; parsed@L488; parse_failed@L489; file@L490; item@L491; tool@L493; allowed@L495; result@L496; item@L508; parse_failed@L509; tool@L513; allowed@L515; result@L516; file@L533; item@L534; parse_failed@L535; tool@L543; result@L545; line@L561; match@L562; tool@L567; result@L570; tool@L571; project@L596; entry@L597; language@L597; work@L598; build@L599; metric@L600; tool@L600; run@L601; metric@L603; tool@L603; build@L604; run@L614; metric@L616; tool@L616; build@L617; run@L632; metric@L634; tool@L634; classes@L635; build@L637; run@L638; metric@L647; tool@L647; build@L648; metric@L650; tool@L650; metric@L653; tool@L653; run@L654; metric@L656; tool@L656; build@L657; metric@L669; tool@L669; name@L670; tree@L671; result@L672; line@L679; metric@L681; tool@L681; result@L683; cases@L686; project_sha256@L687; case@L692; index@L692; output@L693; environment@L694; run@L702; run@L704; bridge_request@L715; request_path@L715; run@L718; result@L733; observation@L734; observation@L740; lines@L764; name@L764; count@L765; line@L765; trace@L769; match@L770; current@L773; row@L774; current@L776; _@L778; hits@L778; line@L778; output@L781; result@L782; lines@L788; name@L788; note@L791; result@L792; file@L795; profiles@L798; result@L799; output@L812; result@L813; output@L828; result@L829; result@L850; row@L851; match@L852; name@L854; existing@L856 +# parameters: path@L49; path@L56; path@L61; project@L77; self@L77; path@L96; self@L96; name@L101; self@L101; value@L101; counts@L106; path@L106; self@L106; code@L117; column@L117; line@L117; message@L117; path@L117; self@L117; severity@L117; cases@L142; expected@L142; metric@L142; self@L142; tool@L142; case_index@L199; evidence@L199; path@L199; evidence@L225; path@L225; evidence@L244; path@L244; evidence@L262; text@L262; node@L283; evidence@L300; command@L331; execute@L331; project@L331; work@L331; evidence@L349; execute@L349; root@L349; command@L594; evidence@L594; execute@L594; root@L594 # """Jason Stys: real polyglot coverage and unused-code adapters, container execution only. @@ -18,9 +18,15 @@ import json import posixpath import re +import sys import xml.etree.ElementTree as ET from pathlib import Path +if __package__: + from .csharp_coverage_bridge import make_request, read_observation +else: + from csharp_coverage_bridge import make_request, read_observation + MAX_REPORT = 8 * 1024 * 1024 SUFFIXES = { "javascript": (".js",), @@ -171,7 +177,10 @@ def coverage(self, tool: str, metric: str, cases: list[dict], expected: int) -> record["truncated"] = record.get("truncated", False) or len(values) != len(record[key]) measured = any(file["status"] == "measured" for file in files.values()) complete = len(cases) == expected and all( - not case.get("timeout") and case.get("exit_code") == case.get("expected_exit_code", 0) + not case.get("timeout") + and case.get("exit_code") == case.get("expected_exit_code", 0) + and case.get("collector_exit_code", 0) == 0 + and not case.get("collector_output_truncated", False) for case in cases ) return { @@ -675,6 +684,11 @@ def measure(evidence: Evidence, root: Path, execute, command: list[str]) -> dict if result["exit_code"] != 0: raise ValueError("Coverage build failed: " + result["stderr"] + result["stdout"]) cases = [] + project_sha256 = hashlib.sha256( + json.dumps(project["files"], sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + ).hexdigest() for index, case in enumerate(project["cases"]): output = root / f"case-{index}.json" environment = {} @@ -698,13 +712,16 @@ def measure(evidence: Evidence, root: Path, execute, command: list[str]) -> dict str(root / "target"), ] elif language == "csharp": + request_path, bridge_request = make_request( + root, project_sha256, index, case["stdin"].encode("utf-8") + ) run = [ "/opt/coverlet/coverlet", str(root / "csharp/App.dll"), "--target", - "/usr/share/dotnet/dotnet", + sys.executable, "--targetargs", - str(root / "csharp/App.dll"), + f"-I /opt/csharp_coverage_bridge.py {request_path.as_posix()}", "--format", "cobertura", "--include-test-assembly", @@ -714,15 +731,32 @@ def measure(evidence: Evidence, root: Path, execute, command: list[str]) -> dict "normal", ] result = execute(run, case["stdin"], timeout=20, env=environment) + observation = None + if language == "csharp": + # A target's expected nonzero return is not a collector error. Only + # the trusted bridge observes that return; Coverlet must itself finish. + if result.get("timeout") or result.get("exit_code") != 0 or result.get("output_truncated"): + raise ValueError("C# coverage collector failed, timed out or truncated output") + observation = read_observation(request_path, bridge_request) cases.append( { "name": case["name"], - "exit_code": result["exit_code"], + "exit_code": observation["target_exit_code"] + if observation is not None + else result["exit_code"], "expected_exit_code": case["exit_code"], "timeout": result.get("timeout", False), } ) if language == "csharp": + cases[-1].update( + { + "target_exit_code": observation["target_exit_code"], + "collector_exit_code": result["exit_code"], + "collector_output_truncated": bool(result.get("output_truncated")), + "bridge": observation, + } + ) cases[-1]["collector_messages"] = (result["stdout"] + result["stderr"])[-2000:] if result.get("timeout") or result["exit_code"] is None: break diff --git a/runner/tools/package-lock.json b/runner/tools/package-lock.json index fa9933b..0ce0115 100644 --- a/runner/tools/package-lock.json +++ b/runner/tools/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-flow-worker-analyzers", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-flow-worker-analyzers", - "version": "0.4.2", + "version": "0.4.3", "license": "MIT", "dependencies": { "@typescript-eslint/eslint-plugin": "8.72.0", diff --git a/runner/tools/package.json b/runner/tools/package.json index 6c7fd72..24a5bdc 100644 --- a/runner/tools/package.json +++ b/runner/tools/package.json @@ -1,6 +1,6 @@ { "name": "code-flow-worker-analyzers", - "version": "0.4.2", + "version": "0.4.3", "private": true, "description": "Fixed, offline worker analyzers; never load target configuration or plugins", "license": "MIT", diff --git a/scripts/compiler_acceptance.py b/scripts/compiler_acceptance.py index 8430388..74f1ff1 100644 --- a/scripts/compiler_acceptance.py +++ b/scripts/compiler_acceptance.py @@ -1,10 +1,10 @@ # # Generated definition map; @L is the exact declaration line. # Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. -# classes: Evidence@L305 -# functions: require(condition, message)@L44; source_digest(text)@L50; source_line_count(text)@L55; validate_coverage_completion(coverage, project)@L60; validate_locations(result, project)@L93; validate_positive(result, project)@L106; negative_project(project, kind)@L207; validate_negative(result, project, kind)@L226; __init__(self, output, provenance)@L308; record(self, language, kind, project, result, error)@L315; save(self)@L337; broker_scenarios(evidence)@L364; anchor_scenarios(evidence)@L385; private_scenarios(evidence)@L412; provenance(runtime, expected_source, lane)@L516; toolchain_versions()@L570; main()@L615 -# variables: RELEASE_SOURCE@L40; RELEASE_WORKER_SHA256@L41; wrapped@L62; cases@L64; actual@L69; expected@L69; wrapped@L85; expected_status@L86; file@L95; sources@L95; item@L97; line@L98; name@L98; compilation@L109; checks@L116; observed@L118; actual@L120; expected@L120; oracle@L130; case@L131; benchmark@L135; samples@L136; value@L139; coverage@L145; entry@L148; content@L149; file@L149; measured@L155; name@L155; file@L158; source@L158; line@L163; executed@L165; missing@L165; summary@L171; covered@L172; extra@L180; first@L184; data@L209; replacement@L215; expected_oracle@L237; case@L238; checks@L241; line@L259; item@L265; case@L276; exit_code@L283; marker@L288; row@L319; key@L333; record@L339; temporary@L340; lines@L343; row@L351; checks@L352; lines@L356; language@L366; baseline@L367; kind@L368; project@L371; result@L372; error@L373; result@L375; error@L381; language@L387; missing_line@L388; project@L388; unused_line@L388; result@L389; error@L390; result@L392; item@L403; error@L408; credentials@L414; key@L415; directory@L416; app@L417; client@L422; jobs@L430; language@L431; project@L432; result@L433; error@L434; response@L436; revision@L438; response@L439; job@L441; deadline@L442; response@L444; report@L446; result@L451; exported@L453; html@L458; error@L465; archives@L469; response@L484; job@L490; result_sha256@L490; response@L491; exported@L498; runtime@L524; source@L529; dirty@L531; worker@L540; command@L545; image@L546; commands@L572; program@L586; name@L591; directory@L592; root@L593; argv@L595; argv@L597; code@L599; output@L599; versions@L601; value@L607; parser@L617; args@L622; evidence@L623; language@L644 -# parameters: condition@L44; message@L44; text@L50; text@L55; coverage@L60; project@L60; project@L93; result@L93; project@L106; result@L106; kind@L207; project@L207; kind@L226; project@L226; result@L226; output@L308; provenance@L308; self@L308; error@L316; kind@L316; language@L316; project@L316; result@L316; self@L316; self@L337; evidence@L364; evidence@L385; evidence@L412; expected_source@L516; lane@L516; runtime@L516 +# classes: Evidence@L348 +# functions: require(condition, message)@L45; source_digest(text)@L51; source_line_count(text)@L56; validate_coverage_completion(coverage, project, historical_source)@L61; validate_locations(result, project)@L136; validate_positive(result, project, historical_source)@L149; negative_project(project, kind)@L250; validate_negative(result, project, kind)@L269; __init__(self, output, provenance)@L351; record(self, language, kind, project, result, error)@L358; save(self)@L380; broker_scenarios(evidence)@L407; anchor_scenarios(evidence)@L434; private_scenarios(evidence)@L465; provenance(runtime, expected_source, lane)@L575; toolchain_versions()@L630; main()@L675 +# variables: RELEASE_SOURCE@L40; RELEASE_WORKER_SHA256@L41; CSHARP_TARGET@L42; wrapped@L66; cases@L68; actual@L73; expected@L73; index@L73; wrapped@L90; bridge@L100; source_hash@L101; file@L103; input_bytes@L109; expected_bridge@L110; expected_status@L129; file@L138; sources@L138; item@L140; line@L141; name@L141; compilation@L152; checks@L159; observed@L161; actual@L163; expected@L163; oracle@L173; case@L174; benchmark@L178; samples@L179; value@L182; coverage@L188; entry@L191; content@L192; file@L192; measured@L198; name@L198; file@L201; source@L201; line@L206; executed@L208; missing@L208; summary@L214; covered@L215; extra@L223; first@L227; data@L252; replacement@L258; expected_oracle@L280; case@L281; checks@L284; line@L302; item@L308; case@L319; exit_code@L326; marker@L331; row@L362; key@L376; record@L382; temporary@L383; lines@L386; row@L394; checks@L395; lines@L399; language@L409; baseline@L410; kind@L411; project@L414; result@L415; error@L416; result@L418; error@L430; language@L436; missing_line@L437; project@L437; unused_line@L437; result@L438; error@L439; result@L441; item@L456; error@L461; credentials@L467; key@L468; directory@L469; app@L470; client@L475; jobs@L483; language@L484; project@L485; result@L486; error@L487; response@L489; revision@L491; response@L492; job@L494; deadline@L495; response@L497; report@L499; result@L504; exported@L512; html@L517; error@L524; archives@L528; response@L543; job@L549; result_sha256@L549; response@L550; exported@L557; runtime@L583; source@L588; dirty@L590; worker@L600; command@L605; image@L606; commands@L632; program@L646; name@L651; directory@L652; root@L653; argv@L655; argv@L657; code@L659; output@L659; versions@L661; value@L667; parser@L677; args@L682; evidence@L683; language@L704 +# parameters: condition@L45; message@L45; text@L51; text@L56; coverage@L62; historical_source@L62; project@L62; project@L136; result@L136; historical_source@L149; project@L149; result@L149; kind@L250; project@L250; kind@L269; project@L269; result@L269; output@L351; provenance@L351; self@L351; error@L359; kind@L359; language@L359; project@L359; result@L359; self@L359; self@L380; evidence@L407; evidence@L434; evidence@L465; expected_source@L575; lane@L575; runtime@L575 # """Jason Stys: real hosted compiler scenarios, independent oracles and private history. @@ -39,6 +39,7 @@ RELEASE_SOURCE = "eb9b1cd0c3e3fd1e70e397edeffb059073464f74" RELEASE_WORKER_SHA256 = "750b665e545b7cf0806d4c493db86174f7f4e7ab2218aa239718e652ccdce158" +CSHARP_TARGET = "/tmp/project/.analysis/csharp/App.dll" # noqa: S108 -- inert original worker path, never a host file. def require(condition: bool, message: str) -> None: @@ -57,8 +58,11 @@ def source_line_count(text: str) -> int: return len(text.replace("\r\n", "\n").replace("\r", "\n").split("\n")) -def validate_coverage_completion(coverage: dict, project: Project) -> None: - """Preserve the documented Coverlet nonzero-exit limitation without inventing full coverage.""" +def validate_coverage_completion( + coverage: dict, project: Project, historical_source: str | None = None +) -> None: + """Require exact current target/collector evidence; preserve only the pinned historical baseline.""" + require(historical_source in (None, RELEASE_SOURCE), "Unknown historical coverage policy") wrapped = False if project.language != "python": cases = coverage.get("cases", []) @@ -66,7 +70,7 @@ def validate_coverage_completion(coverage: dict, project: Project) -> None: coverage.get("requested_cases") == len(project.cases) and len(cases) == len(project.cases), "Missing instrumented case evidence", ) - for expected, actual in zip(project.cases, cases, strict=True): + for index, (expected, actual) in enumerate(zip(project.cases, cases, strict=True)): require( actual.get("name") == expected.name and actual.get("expected_exit_code") == expected.exit_code @@ -75,7 +79,8 @@ def validate_coverage_completion(coverage: dict, project: Project) -> None: ) if actual.get("exit_code") != expected.exit_code: require( - project.language == "csharp" + historical_source == RELEASE_SOURCE + and project.language == "csharp" and coverage.get("tool") == "Coverlet 8.0.1" and expected.exit_code != 0 and actual.get("exit_code") == 1 @@ -83,6 +88,44 @@ def validate_coverage_completion(coverage: dict, project: Project) -> None: "Unexpected instrumented execution failure", ) wrapped = True + if project.language == "csharp" and historical_source is None: + require( + type(actual.get("collector_exit_code")) is int + and actual["collector_exit_code"] == 0 + and type(actual.get("target_exit_code")) is int + and actual["target_exit_code"] == actual.get("exit_code") + and not actual.get("collector_output_truncated"), + "Current C# collector/target exits are not independently complete", + ) + bridge = actual.get("bridge", {}) + source_hash = source_digest( + json.dumps( + [file.model_dump() for file in project.files], + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ) + ) + input_bytes = expected.stdin.encode("utf-8") + expected_bridge = ( + Path(studio.__file__).resolve().parent.parent / "runner/csharp_coverage_bridge.py" + ) + require( + type(bridge.get("schema")) is int + and bridge["schema"] == 1 + and type(bridge.get("case_index")) is int + and bridge["case_index"] == index + and bridge.get("target") == CSHARP_TARGET + and bridge.get("source_sha256") == source_hash + and bridge.get("stdin_sha256") == hashlib.sha256(input_bytes).hexdigest() + and type(bridge.get("stdin_bytes")) is int + and bridge["stdin_bytes"] == len(input_bytes) + and type(bridge.get("target_exit_code")) is int + and bridge["target_exit_code"] == expected.exit_code + and bridge.get("bridge_sha256") + == hashlib.sha256(expected_bridge.read_bytes()).hexdigest(), + "Current C# bridge is not bound to this source/case/input/helper", + ) expected_status = "partial" if wrapped else "measured" require( coverage.get("status") == expected_status, @@ -103,7 +146,7 @@ def validate_locations(result: dict, project: Project) -> None: require(item.get("source_sha256") == source_digest(sources[name]), "Diagnostic source hash mismatch") -def validate_positive(result: dict, project: Project) -> None: +def validate_positive(result: dict, project: Project, *, historical_source: str | None = None) -> None: """Require actual compilation, every approved output, measurements and source-bound coverage.""" require(result.get("tool_status") == "completed", "Worker did not complete") compilation = result.get("compilation", {}) @@ -143,7 +186,7 @@ def validate_positive(result: dict, project: Project) -> None: type(benchmark.get("max_rss_kib")) is int and benchmark["max_rss_kib"] > 0, "Missing measured RSS" ) coverage = result.get("coverage", {}) - validate_coverage_completion(coverage, project) + validate_coverage_completion(coverage, project, historical_source) require(not coverage.get("locations_truncated"), "Coverage locations were truncated") entry = coverage.get("files", {}).get(project.entry, {}) content = next(file.content for file in project.files if file.name == project.entry) @@ -374,7 +417,13 @@ async def broker_scenarios(evidence: Evidence) -> None: try: result = await execution.run(project, local_only=True) if kind == "positive": - validate_positive(result, project) + validate_positive( + result, + project, + historical_source=RELEASE_SOURCE + if evidence.provenance["lane"] == "release-0.4.2" + else None, + ) else: validate_negative(result, project, kind) except (OSError, ValueError, KeyError, TypeError) as failure: @@ -390,7 +439,11 @@ async def anchor_scenarios(evidence: Evidence) -> None: error = None try: result = await execution.run(project, local_only=True) - validate_positive(result, project) + validate_positive( + result, + project, + historical_source=RELEASE_SOURCE if evidence.provenance["lane"] == "release-0.4.2" else None, + ) require( missing_line in result["coverage"]["files"][project.entry]["missing_lines"], "Authored uncovered anchor was not measured as missing", @@ -449,7 +502,13 @@ def private_scenarios(evidence: Evidence) -> None: time.sleep(0.5) require(report["status"] == "completed", "Private job did not complete") result = report["result"] - validate_positive(result, project) + validate_positive( + result, + project, + historical_source=RELEASE_SOURCE + if evidence.provenance["lane"] == "release-0.4.2" + else None, + ) exported = client.get(f"/private/api/runs/{job}/report?format=json") require( exported.status_code == 200 and exported.json()["result"] == result, @@ -534,7 +593,8 @@ def provenance(runtime: Path, expected_source: str, lane: str) -> dict: ).strip() require(not dirty, "Runtime tracked source is dirty") require( - studio.__version__ == "0.4.2" and execution.IMAGE == "code-flow-worker:0.4.2", + studio.__version__ == ("0.4.2" if lane == "release-0.4.2" else "0.4.3") + and execution.IMAGE == f"code-flow-worker:{studio.__version__}", "Wrong product/worker version", ) worker = hashlib.sha256((runtime / "runner/node_checks.cjs").read_bytes()).hexdigest() diff --git a/scripts/csharp_coverage_checks.py b/scripts/csharp_coverage_checks.py new file mode 100644 index 0000000..652daf4 --- /dev/null +++ b/scripts/csharp_coverage_checks.py @@ -0,0 +1,279 @@ +# +# Generated definition map; @L is the exact declaration line. +# Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. +# functions: exit_project()@L39; collector_fault(mode)@L62; validate_collector_fault(record)@L137; checks(record, output)@L172; save()@L175; main()@L244 +# variables: PROGRAM@L29; code@L48; expected@L48; name@L48; value@L48; project@L65; program@L76; name@L107; directory@L108; root@L109; argv@L111; argv@L113; output@L115; status@L115; results@L117; line@L119; observations@L122; line@L124; observed@L128; result@L128; mode@L139; observed@L139; result@L139; temporary@L177; project@L181; result@L182; case@L188; changed@L192; unexpected@L194; case@L203; fault@L222; mode@L236; parser@L246; args@L249; root@L250; identity@L251; bridge@L252; record@L253; output@L260 +# parameters: mode@L62; record@L137; output@L172; record@L172 +# + +"""Jason Stys: hosted-only actual .NET/Coverlet exit and collector-failure acceptance. + +Healthy target rejection is distinct from a failed collector. Local tests may +inspect inert manifests; main refuses compiler/container execution on this PC. +""" + +import argparse +import asyncio +import contextlib +import hashlib +import json +import tempfile +from pathlib import Path +from uuid import uuid4 + +import studio +from scripts.compiler_acceptance import provenance, require, validate_positive +from studio import execution +from studio.models import Project + +PROGRAM = """// Return an explicitly requested CLI status; malformed input is an approved rejection. +using System.Globalization; +string text = Console.In.ReadToEnd().Trim(); +int code = int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out int parsed) + && parsed <= 255 ? parsed : 2; +Console.WriteLine($"exit={code}"); +return code; +""" + + +def exit_project() -> Project: + """Independent literal oracles cover ordinary success and distinct approved nonzero returns.""" + return Project( + title="C# exact instrumented exit contract", + language="csharp", + entry="Program.cs", + files=[{"name": "Program.cs", "content": PROGRAM}], + cases=[ + {"name": name, "stdin": value, "stdout": expected, "exit_code": code} + for name, value, expected, code in [ + ("success-zero", "0\n", "exit=0", 0), + ("expected-one", "1\n", "exit=1", 1), + ("expected-two", "2\n", "exit=2", 2), + ("expected-seven", "7\n", "exit=7", 7), + ("expected-255", "255\n", "exit=255", 255), + ("malformed-input", "invalid\n", "exit=2", 2), + ] + ], + fuzz_cases=0, + benchmark_repeats=1, + ) + + +async def collector_fault(mode: str) -> dict: + """Invoke the real worker/Coverlet with one trusted collector-only fault, preserving sandbox flags.""" + require(mode in {"invalid-option", "unwritable-output", "timeout"}, "Unknown collector fault") + project = Project( + title="Controlled collector failure", + language="csharp", + entry="Program.cs", + files=[{"name": "Program.cs", "content": 'Console.WriteLine("collector-control");\n'}], + cases=[{"name": "healthy-target", "stdin": "", "stdout": "collector-control", "exit_code": 0}], + fuzz_cases=0, + benchmark_repeats=1, + ) + # This fixed CI probe wraps only the existing trusted execute callback. It + # does not add an application API or allow source files to select commands. + program = """import sys,json +from pathlib import Path +sys.path.insert(0,"/opt") +import worker +from csharp_coverage_bridge import read_observation +original=worker.execute +MODE=MODE_VALUE +def collect(argv,*args,**kwargs): + if argv and argv[0]=="/opt/coverlet/coverlet": + if MODE=="invalid-option": + argv=[*argv,"--cfs-intentionally-invalid-collector-option"] + elif MODE=="unwritable-output": + argv=list(argv) + argv[argv.index("--output")+1]="/opt/cfs-intentionally-unwritable-coverage.xml" + else: + kwargs["timeout"]=0.001 + observed=original(argv,*args,**kwargs) + text=observed.get("stdout","")+observed.get("stderr","") + evidence={"exit_code":observed.get("exit_code"),"timeout":observed.get("timeout",False),"option_rejected":"Unrecognized" in text and "--cfs-intentionally-invalid-collector-option" in text,"collector_tail":text[-1024:],"valid_target_observation":False} + for request in Path("/tmp/project/.analysis").glob("csharp-request-*.json"): + try: + record=read_observation(request,json.loads(request.read_text())) + evidence.update(valid_target_observation=True,target_exit_code=record["target_exit_code"]) + except (OSError,ValueError): + continue + print("CFS_COLLECTOR_FAULT="+json.dumps(evidence),flush=True) + return observed + return original(argv,*args,**kwargs) +worker.execute=collect +worker.main() +""".replace("MODE_VALUE", repr(mode)) + name = "code-flow-csharp-fault-" + uuid4().hex + with tempfile.TemporaryDirectory(prefix="code-flow-csharp-fault-") as directory: + root = Path(directory) + (root / "job.json").write_text(project.model_dump_json(), encoding="utf-8") + argv = execution.container_arguments(root, name) + argv.insert(-1, "--entrypoint=python") + argv += ["-I", "-c", program] + try: + status, output = await execution.command(argv) + require(status == 0, "Collector fault worker failed outside its report contract") + results = [ + line.removeprefix("STUDIO_RESULT=") + for line in output.splitlines() + if line.startswith("STUDIO_RESULT=") + ] + observations = [ + line.removeprefix("CFS_COLLECTOR_FAULT=") + for line in output.splitlines() + if line.startswith("CFS_COLLECTOR_FAULT=") + ] + require(len(results) == len(observations) == 1, "Collector fault evidence is missing/ambiguous") + result, observed = json.loads(results[0]), json.loads(observations[0]) + return {"fault": mode, "actual_collector": observed, "worker_result": result} + finally: + with contextlib.suppress(OSError, TimeoutError, ValueError): + await execution.command( + [execution.DOCKER, "rm", "--force", name], timeout=10, output_limit=4096 + ) + + +def validate_collector_fault(record: dict) -> None: + """Require a real observed fault before calling its correctly blocked coverage a pass.""" + mode, observed, result = record["fault"], record["actual_collector"], record["worker_result"] + require(mode in {"invalid-option", "unwritable-output", "timeout"}, "Unknown collector fault") + if mode == "timeout": + require(observed.get("timeout") is True, "Real collector timeout was not observed") + elif mode == "invalid-option": + # Coverlet Main ignores the parser's return and may exit zero without + # ever invoking its target. Missing observation must still block it. + require( + observed.get("option_rejected") is True + and observed.get("valid_target_observation") is False + and not observed.get("timeout"), + "Collector argument failure was not observed", + ) + else: + require( + observed.get("exit_code") not in (None, 0) + and not observed.get("timeout") + and observed.get("valid_target_observation") is True + and observed.get("target_exit_code") == 0, + "Post-target collector report-write failure was not observed", + ) + require( + result.get("tool_status") == "completed" + and result.get("checks", {}).get("passed") == 1 + and result["checks"].get("failed") == 0, + "Fault injection also changed healthy target behavior", + ) + require( + result.get("coverage", {}).get("status") == "blocked", + "Collector failure was falsely accepted as measured coverage", + ) + + +async def checks(record: dict, output: Path) -> None: + """Keep real target, mismatched expectation and real collector failures separate in evidence.""" + + def save(): + """Retain each completed synthetic case before any subsequent assertion can fail.""" + temporary = output.with_suffix(".tmp") + temporary.write_text(json.dumps(record, indent=2, allow_nan=False) + "\n", encoding="utf-8") + temporary.replace(output) + + project = exit_project() + result = await execution.run(project, local_only=True) + record["exact_exits"] = {"project": project.model_dump(), "result": result} + save() + validate_positive(result, project) + require( + result["coverage"]["status"] == "measured" + and [case["target_exit_code"] for case in result["coverage"]["cases"]] == [0, 1, 2, 7, 255, 2], + "Instrumented target exit statuses were collapsed or coverage remains partial", + ) + + changed = project.model_dump() + changed["cases"] = [{"name": "unexpected-nonzero", "stdin": "7\n", "stdout": "exit=7", "exit_code": 2}] + unexpected = await execution.run(Project.model_validate(changed), local_only=True) + record["unexpected_exit"] = unexpected + save() + require( + unexpected.get("tool_status") == "completed" + and unexpected.get("checks", {}).get("failed") == 1 + and unexpected["checks"].get("passed") == 0, + "Unexpected target exit was not a failed case", + ) + case = unexpected.get("coverage", {}).get("cases", [{}])[0] + require( + unexpected["coverage"].get("status") == "partial" + and case.get("exit_code") == case.get("target_exit_code") == 7 + and case.get("expected_exit_code") == 2 + and case.get("collector_exit_code") == 0, + "Unexpected target and collector outcomes were confused", + ) + + changed["files"] = [ + { + "name": "Program.cs", + "content": 'throw new InvalidOperationException("Synthetic C# runtime failure");\n', + "encoding": "text", + } + ] + changed["cases"] = [ + {"name": "unexpected-exception", "stdin": "", "stdout": "unreachable", "exit_code": 0} + ] + fault = await execution.run(Project.model_validate(changed), local_only=True) + record["unexpected_exception"] = fault + save() + require( + fault.get("checks", {}).get("failed") == 1 + and fault["checks"]["cases"][0].get("exit_code") not in (None, 0) + and "Synthetic C# runtime failure" in fault["checks"]["cases"][0].get("stderr", ""), + "Authored runtime exception was not observed", + ) + require( + fault.get("coverage", {}).get("status") in {"partial", "blocked"}, + "Unexpected exception was falsely measured as a completed approved run", + ) + + for mode in ("invalid-option", "unwritable-output", "timeout"): + record["collector_" + mode] = await collector_fault(mode) + save() + validate_collector_fault(record["collector_" + mode]) + record["acceptance"] = "passed" + save() + + +def main() -> None: + """Refuse local Docker; identify the exact current runtime and exercise the real hosted tools.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runtime-source", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + root = Path(studio.__file__).resolve().parent.parent + identity = provenance(root, args.runtime_source, "current") + bridge = root / "runner/csharp_coverage_bridge.py" + record = { + "schema": 1, + "provenance": identity, + "bridge_sha256": hashlib.sha256(bridge.read_bytes()).hexdigest(), + "acceptance": "pending", + } + args.output.mkdir(parents=True, exist_ok=True) + output = args.output / "csharp-coverage.json" + asyncio.run(checks(record, output)) + print( + "CSHARP_COVERAGE_ACCEPTANCE=" + + json.dumps( + { + "source": args.runtime_source, + "version": studio.__version__, + "target_exits": [0, 1, 2, 7, 255, 2], + "coverage": "measured", + "collector_errors_and_timeout": "blocked", + "acceptance": record["acceptance"], + } + ), + flush=True, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/released_service_probe.py b/scripts/released_service_probe.py index e62abc7..960f717 100644 --- a/scripts/released_service_probe.py +++ b/scripts/released_service_probe.py @@ -570,7 +570,7 @@ def exercise(service: Service, data: Path, evidence: Evidence) -> dict: require(time.monotonic() < deadline, "Released worker job deadline exceeded") time.sleep(0.25) require(result.get("status") == "completed", "Released worker did not complete") - validate_positive(result.get("result", {}), project) + validate_positive(result.get("result", {}), project, historical_source=SOURCE) exported = request_json(client, "GET", f"/private/api/runs/{job}/report?format=json") require( result.get("id") == job and result.get("revision_id") == identifier, diff --git a/studio/__init__.py b/studio/__init__.py index 22a39b6..689cf18 100644 --- a/studio/__init__.py +++ b/studio/__init__.py @@ -6,4 +6,4 @@ """Code Flow Studio orchestration package. Symbol locations: docs/code-index.md.""" -__version__ = "0.4.2" +__version__ = "0.4.3" diff --git a/studio/execution.py b/studio/execution.py index 341d299..2f5798e 100644 --- a/studio/execution.py +++ b/studio/execution.py @@ -25,7 +25,7 @@ from studio.locations import tool_locations from studio.models import Project -IMAGE = "code-flow-worker:0.4.2" +IMAGE = "code-flow-worker:0.4.3" DOCKER = os.environ.get("STUDIO_DOCKER", shutil.which("docker") or "docker") diff --git a/tests/desktop.spec.ts b/tests/desktop.spec.ts index 24de658..6fa8596 100644 --- a/tests/desktop.spec.ts +++ b/tests/desktop.spec.ts @@ -1,7 +1,7 @@ /* * Generated definition map; @L is the exact declaration line. * Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. - * variables: { verifyElectron }@L24; { readDiagnosticLog }@L27; executable@L33; app@L36; launcherDirectory@L54; acceptedPackage@L57; resources@L64; file@L65; metadata@L98; page@L118; accountStartedAt@L121; accountRoutes@L122; route@L124; route@L136; browserErrors@L148; health@L152; state@L160; directory@L191; serviceLog@L194; statuses@L195; lifecycle@L260; records@L261; launcher@L268; line@L271; service@L276; line@L277; diagnostic@L284; records@L288; stage@L289; record@L301; pid@L314 + * variables: { verifyElectron }@L24; { readDiagnosticLog }@L27; executable@L33; app@L36; launcherDirectory@L54; acceptedPackage@L57; resources@L64; file@L65; file@L91; metadata@L103; page@L123; accountStartedAt@L126; accountRoutes@L127; route@L129; route@L141; browserErrors@L153; health@L157; state@L165; directory@L196; serviceLog@L199; statuses@L200; lifecycle@L265; records@L266; launcher@L273; line@L276; service@L281; line@L282; diagnostic@L289; records@L293; stage@L294; record@L306; pid@L319 * */ /** Verify the actual Electron shell and bundled Python service, without faking readiness. @@ -66,6 +66,7 @@ test("bundled desktop authenticates, seals history and provides a sandboxed edit "Dockerfile", "worker.py", "language_checks.py", + "csharp_coverage_bridge.py", "node_checks.cjs", "sql_runtime.py", "php_coverage.php", @@ -85,16 +86,20 @@ test("bundled desktop authenticates, seals history and provides a sandboxed edit expect( existsSync(join(resources, "runner", "tools", "node_modules")), ).toBe(false); - // Prove the actual distributed worker resource is this reviewed source, not an older existence-only copy. - expect( - createHash("sha256") - .update(readFileSync(join(resources, "runner", "node_checks.cjs"))) - .digest("hex"), - ).toBe( - createHash("sha256") - .update(readFileSync("runner/node_checks.cjs")) - .digest("hex"), - ); + // Bind the actual packaged analyzers and C# bridge to this reviewed source. + // Metadata schema 1 retains its legacy Node-only worker_sha256 field. + for (const file of ["node_checks.cjs", "csharp_coverage_bridge.py"]) { + expect( + createHash("sha256") + .update(readFileSync(join(resources, "runner", file))) + .digest("hex"), + `Bundled worker digest: ${file}`, + ).toBe( + createHash("sha256") + .update(readFileSync(join("runner", file))) + .digest("hex"), + ); + } const metadata = JSON.parse( readFileSync( join(resources, "metadata", "source-release.json"), @@ -103,7 +108,7 @@ test("bundled desktop authenticates, seals history and provides a sandboxed edit ); expect(metadata.schema).toBe(1); expect(metadata.product).toBe("code-flow-studio"); - expect(metadata.version).toBe("0.4.2"); + expect(metadata.version).toBe("0.4.3"); expect(metadata.source).toMatch(/^[a-f0-9]{40}$/); expect(typeof metadata.source_dirty).toBe("boolean"); expect(metadata.worker_sha256).toBe( @@ -150,7 +155,7 @@ test("bundled desktop authenticates, seals history and provides a sandboxed edit if (browserErrors.length < 8) browserErrors.push(error.name); }); const health = await page.request.get(new URL("/health", page.url()).href); - expect((await health.json()).version).toBe("0.4.2"); + expect((await health.json()).version).toBe("0.4.3"); try { console.info("NATIVE_JOURNEY_STAGE", "account:start"); await createSession(page, "native_fictional"); @@ -332,7 +337,7 @@ test("bundled desktop authenticates, seals history and provides a sandboxed edit "artifacts/native-resource-acceptance.json", JSON.stringify({ schema: 1, - version: "0.4.2", + version: "0.4.3", source: acceptedPackage.source, worker_sha256: acceptedPackage.worker_sha256, compiled_app: true, diff --git a/tests/test_compiler_acceptance.py b/tests/test_compiler_acceptance.py index d0a7d4e..6e39b63 100644 --- a/tests/test_compiler_acceptance.py +++ b/tests/test_compiler_acceptance.py @@ -1,9 +1,9 @@ # # Generated definition map; @L is the exact declaration line. # Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. -# functions: complete_result(project)@L24; test_practical_matrix_is_complete_and_independent()@L80; test_positive_validator_rejects_incomplete_or_fabricated_evidence(defect)@L124; test_exact_compiler_location_and_nonexecution_are_required()@L178; test_runtime_failure_is_distinct_from_incorrect_oracle()@L211; test_real_execution_guard_refuses_this_pc_before_subprocess(monkeypatch)@L247; test_actual_first_hosted_reports_retain_their_real_tool_semantics(language, kind)@L258 -# variables: content@L26; case@L42; case@L46; case@L63; language@L84; project@L85; case@L87; duplicate@L91; project@L126; result@L127; project@L180; result@L181; defect@L199; changed@L200; project@L213; result@L214; wrong@L236; observed@L262; item@L265; row@L265; project@L266; changed@L271; changed@L275 -# parameters: project@L24; defect@L124; monkeypatch@L247; kind@L258; language@L258 +# functions: complete_result(project)@L30; test_practical_matrix_is_complete_and_independent()@L119; test_positive_validator_rejects_incomplete_or_fabricated_evidence(defect)@L163; test_exact_compiler_location_and_nonexecution_are_required()@L217; test_runtime_failure_is_distinct_from_incorrect_oracle()@L250; test_real_execution_guard_refuses_this_pc_before_subprocess(monkeypatch)@L286; test_actual_first_hosted_reports_retain_their_real_tool_semantics(language, kind)@L297; test_current_csharp_rejects_the_historical_partial_allowance()@L320; test_current_csharp_requires_separate_source_bound_exit_observation(defect)@L331 +# variables: content@L32; result@L33; case@L48; case@L52; case@L69; source_hash@L85; file@L87; bridge_hash@L93; case@L96; index@L96; record@L96; raw@L97; language@L123; project@L124; case@L126; duplicate@L130; project@L165; result@L166; project@L219; result@L220; defect@L238; changed@L239; project@L252; result@L253; wrong@L275; observed@L301; item@L304; row@L304; project@L305; changed@L310; changed@L314; observed@L322; item@L323; row@L323; project@L333; result@L334; case@L335 +# parameters: project@L30; defect@L163; monkeypatch@L286; kind@L297; language@L297; defect@L331 # """Jason Stys: reject fabricated/incomplete compiler evidence without using local Docker.""" @@ -17,14 +17,20 @@ import pytest from examples.practical_projects import CASES, PROGRAMS, RUNTIME_FAILURES, SYNTAX_FAILURES, practical_project -from scripts.compiler_acceptance import negative_project, provenance, validate_negative, validate_positive +from scripts.compiler_acceptance import ( + CSHARP_TARGET, + negative_project, + provenance, + validate_negative, + validate_positive, +) from studio.models import LANGUAGES def complete_result(project): """A minimal synthetic evidence record for validator mutation tests, never toolchain proof.""" content = project.files[0].content - return { + result = { "tool_status": "completed", "compilation": {"exit_code": 0}, "checks": { @@ -75,6 +81,39 @@ def complete_result(project): "unused": {"status": "completed"}, "static": {"files": {project.entry: {"parsed": True}}}, } + if project.language == "csharp": + source_hash = hashlib.sha256( + json.dumps( + [file.model_dump() for file in project.files], + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode() + ).hexdigest() + bridge_hash = hashlib.sha256( + (Path(__file__).parents[1] / "runner/csharp_coverage_bridge.py").read_bytes() + ).hexdigest() + for index, (case, record) in enumerate(zip(project.cases, result["coverage"]["cases"], strict=True)): + raw = case.stdin.encode() + record.update( + { + "target_exit_code": case.exit_code, + "collector_exit_code": 0, + "collector_output_truncated": False, + "bridge": { + "schema": 1, + "nonce": "a" * 64, + "case_index": index, + "source_sha256": source_hash, + "stdin_sha256": hashlib.sha256(raw).hexdigest(), + "stdin_bytes": len(raw), + "target": CSHARP_TARGET, + "target_exit_code": case.exit_code, + "bridge_sha256": bridge_hash, + }, + } + ) + return result def test_practical_matrix_is_complete_and_independent(): @@ -264,15 +303,45 @@ def test_actual_first_hosted_reports_retain_their_real_tool_semantics(language, ) row = next(item for item in observed["reports"] if item["language"] == language and item["kind"] == kind) project = Project.model_validate(row["project"]) - validate_positive(row["result"], project) + validate_positive(row["result"], project, historical_source=observed["source"]) if language == "csharp": # Do not disguise the collector's expected nonzero wrapper result as full coverage. assert row["result"]["coverage"]["status"] == "partial" changed = copy.deepcopy(row["result"]) changed["coverage"]["status"] = "measured" with pytest.raises(ValueError, match="collector exits"): - validate_positive(changed, project) + validate_positive(changed, project, historical_source=observed["source"]) changed = copy.deepcopy(row["result"]) changed["coverage"]["cases"][0]["exit_code"] = 1 with pytest.raises(ValueError, match="Unexpected instrumented"): - validate_positive(changed, project) + validate_positive(changed, project, historical_source=observed["source"]) + + +def test_current_csharp_rejects_the_historical_partial_allowance(): + """Current workers cannot opt into old partial completion by copying a tool name/message.""" + observed = json.loads((Path(__file__).parent / "fixtures/compiler-reports-042.json").read_text()) + row = next(item for item in observed["reports"] if item["language"] == "csharp") + from studio.models import Project + + with pytest.raises(ValueError): + validate_positive(row["result"], Project.model_validate(row["project"])) + + +@pytest.mark.parametrize("defect", ["collector", "target", "binding", "helper", "false-complete"]) +def test_current_csharp_requires_separate_source_bound_exit_observation(defect): + """A collector success or an expected-code guess cannot replace the bridge observation.""" + project = practical_project("csharp") + result = complete_result(project) + case = result["coverage"]["cases"][0] + if defect == "collector": + case["collector_exit_code"] = 1 + elif defect == "target": + case["target_exit_code"] = 7 + elif defect == "binding": + case["bridge"]["case_index"] = 5 + elif defect == "helper": + case["bridge"]["bridge_sha256"] = "0" * 64 + else: + result["coverage"]["status"] = "partial" + with pytest.raises(ValueError): + validate_positive(result, project) diff --git a/tests/test_csharp_collector_controls.py b/tests/test_csharp_collector_controls.py new file mode 100644 index 0000000..e89b592 --- /dev/null +++ b/tests/test_csharp_collector_controls.py @@ -0,0 +1,63 @@ +# +# Generated definition map; @L is the exact declaration line. +# Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. +# functions: record(mode)@L18; test_observed_fault_is_required_and_must_be_blocked(mode)@L39; test_write_failure_must_follow_valid_successful_target_observation()@L58 +# variables: observed@L20; actual@L41; changed@L43; changed@L47; actual@L60 +# parameters: mode@L18; mode@L39 +# + +"""Jason Stys: fault-injection evidence must prove the intended actual collector fault.""" + +import copy + +import pytest + +from scripts.csharp_coverage_checks import validate_collector_fault + + +def record(mode): + """Inert records model three distinct fault contracts, not actual .NET execution.""" + observed = {"exit_code": 0, "timeout": False, "option_rejected": False, "valid_target_observation": False} + if mode == "invalid-option": + observed["option_rejected"] = True + elif mode == "unwritable-output": + observed.update(exit_code=3, valid_target_observation=True, target_exit_code=0) + else: + observed.update(exit_code=-9, timeout=True) + return { + "fault": mode, + "actual_collector": observed, + "worker_result": { + "tool_status": "completed", + "checks": {"passed": 1, "failed": 0}, + "coverage": {"status": "blocked"}, + }, + } + + +@pytest.mark.parametrize("mode", ["invalid-option", "unwritable-output", "timeout"]) +def test_observed_fault_is_required_and_must_be_blocked(mode): + """Healthy output, missing actual fault, or measured coverage cannot satisfy a fault control.""" + actual = record(mode) + validate_collector_fault(actual) + changed = copy.deepcopy(actual) + changed["worker_result"]["coverage"]["status"] = "measured" + with pytest.raises(ValueError, match="falsely accepted"): + validate_collector_fault(changed) + changed = copy.deepcopy(actual) + if mode == "invalid-option": + changed["actual_collector"]["option_rejected"] = False + elif mode == "unwritable-output": + changed["actual_collector"]["exit_code"] = 0 + else: + changed["actual_collector"]["timeout"] = False + with pytest.raises(ValueError, match="not observed"): + validate_collector_fault(changed) + + +def test_write_failure_must_follow_valid_successful_target_observation(): + """An unrelated collector/target launch failure cannot stand in for a report-write failure.""" + actual = record("unwritable-output") + actual["actual_collector"]["valid_target_observation"] = False + with pytest.raises(ValueError, match="Post-target"): + validate_collector_fault(actual) diff --git a/tests/test_csharp_coverage_bridge.py b/tests/test_csharp_coverage_bridge.py new file mode 100644 index 0000000..cf4ee5c --- /dev/null +++ b/tests/test_csharp_coverage_bridge.py @@ -0,0 +1,343 @@ +# +# Generated definition map; @L is the exact declaration line. +# Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. +# functions: analysis(monkeypatch, tmp_path)@L34; request(analysis, body, case_index)@L46; stub_observation(monkeypatch, analysis, body, exit_code)@L51; test_request_binds_full_unicode_byte_input_and_never_overwrites(analysis)@L61; test_request_invalid_context_rejected_before_process(analysis, kind)@L92; test_forced_request_name_collision_preserves_original(monkeypatch, analysis)@L112; test_actual_inert_child_exit_and_binary_stdin_are_preserved(analysis, exit_code)@L123; test_actual_inert_signal_exit_is_not_normalized_to_collector_exit(analysis)@L146; test_receipt_is_absent_until_process_wait_returns_and_argv_has_no_shell(monkeypatch, analysis)@L154; run(argv)@L161; test_changed_stdin_never_starts_target(monkeypatch, analysis)@L175; test_request_changed_during_target_cannot_publish_completion(monkeypatch, analysis)@L186; run(argv)@L190; test_missing_observation_never_looks_like_zero_exit(analysis)@L201; test_malformed_request_cannot_launch_the_target(monkeypatch, analysis, defect)@L221; test_existing_receipt_cannot_be_replaced_or_execute_again(monkeypatch, analysis)@L251; test_observation_tamper_and_malformed_bytes_rejected(monkeypatch, analysis, defect)@L283; test_reader_rejects_changed_expected_request(monkeypatch, analysis)@L317; test_aliasing_paths_are_not_accepted_as_owned_regular_receipts(monkeypatch, analysis, kind)@L327 +# variables: SOURCE@L30; root@L36; target@L37; path@L53; value@L53; body@L63; path@L64; value@L64; another@L84; different@L84; body@L94; index@L94; source@L94; source@L96; source@L98; index@L100; index@L102; index@L104; body@L106; _value@L115; path@L115; original@L116; body@L125; witness@L126; path@L133; value@L133; observed@L136; key@L140; key@L141; val@L141; path@L149; value@L149; body@L156; path@L157; value@L157; receipt@L158; calls@L159; path@L177; value@L177; path@L188; value@L188; altered@L191; path@L203; value@L203; path@L223; value@L223; altered@L224; changes@L225; changed@L233; field@L233; content@L235; content@L237; content@L239; content@L241; path@L253; receipt@L253; value@L253; original@L254; path@L285; receipt@L285; value@L285; observed@L286; changes@L287; changed@L301; key@L301; body@L303; body@L305; body@L307; body@L309; body@L311; _receipt@L319; path@L319; value@L319; changed@L320; path@L329; receipt@L329; value@L329; selected@L330; alias@L331; original@L335 +# parameters: monkeypatch@L34; tmp_path@L34; analysis@L46; body@L46; case_index@L46; analysis@L51; body@L51; exit_code@L51; monkeypatch@L51; args@L55; kwargs@L55; analysis@L61; analysis@L92; kind@L92; analysis@L112; monkeypatch@L112; count@L114; analysis@L123; exit_code@L123; analysis@L146; analysis@L154; monkeypatch@L154; argv@L161; kwargs@L161; analysis@L175; monkeypatch@L175; args@L179; kwargs@L179; analysis@L186; monkeypatch@L186; argv@L190; kwargs@L190; analysis@L201; analysis@L221; defect@L221; monkeypatch@L221; args@L244; kwargs@L244; analysis@L251; monkeypatch@L251; args@L256; kwargs@L256; analysis@L283; defect@L283; monkeypatch@L283; analysis@L317; monkeypatch@L317; analysis@L327; kind@L327; monkeypatch@L327 +# + +"""Jason Stys: independent bridge receipt/process regressions; no Docker, compiler or submitted code. + +Actual child tests execute only this file's inert Python fixture under the fixed +target interface. Shipping Coverlet/.NET acceptance remains separate hosted CI. +Symbol and variable locations are maintained in docs/code-index.md. +""" + +import copy +import hashlib +import io +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +import pytest + +from runner import csharp_coverage_bridge as bridge + +SOURCE = hashlib.sha256(b"independent synthetic source").hexdigest() + + +@pytest.fixture +def analysis(monkeypatch, tmp_path): + """Replace only trusted fixed tool paths with an owned temporary inert Python target.""" + root = tmp_path / ".analysis" + target = root / "csharp" / "App.dll" + target.parent.mkdir(parents=True) + target.write_text("pass\n", encoding="utf-8") + monkeypatch.setattr(bridge, "ANALYSIS", root) + monkeypatch.setattr(bridge, "ASSEMBLY", target) + monkeypatch.setattr(bridge, "DOTNET", Path(sys.executable)) + return root + + +def request(analysis, body=b"approved stdin", case_index=0): + """Construct the real on-disk request with independent authored source/input values.""" + return bridge.make_request(analysis, SOURCE, case_index, body) + + +def stub_observation(monkeypatch, analysis, body=b"approved stdin", exit_code=0): + """Run the actual bridge writer around a nonexecuting process double, then read real bytes.""" + path, value = request(analysis, body) + monkeypatch.setattr( + bridge.subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(args[0], exit_code) + ) + assert bridge.observe(path, io.BytesIO(body)) == 0 + return path, value, analysis / value["receipt"] + + +def test_request_binds_full_unicode_byte_input_and_never_overwrites(analysis): + """The 8192-character four-byte Unicode boundary is 32768 bytes, not 8192 bytes.""" + body = ("\U0001f680" * 8192).encode() + path, value = request(analysis, body, 63) + assert path.parent == analysis + assert re.fullmatch(r"csharp-request-[a-f0-9]{32}\.json", path.name) + assert value == json.loads(path.read_bytes()) + assert set(value) == { + "schema", + "nonce", + "case_index", + "source_sha256", + "stdin_sha256", + "stdin_bytes", + "receipt", + } + assert type(value["schema"]) is int and value["schema"] == 1 + assert type(value["case_index"]) is int and value["case_index"] == 63 + assert value["source_sha256"] == SOURCE + assert value["stdin_bytes"] == 32768 + assert value["stdin_sha256"] == hashlib.sha256(body).hexdigest() + assert re.fullmatch(r"[a-f0-9]{64}", value["nonce"]) + assert value["receipt"] == path.name.replace("csharp-request-", "csharp-exit-") + another, different = request(analysis, body, 63) + assert another != path and different["nonce"] != value["nonce"] + + +@pytest.mark.parametrize( + "kind", + ["source-short", "source-uppercase", "case-bool", "case-negative", "case-large", "input-too-large"], +) +def test_request_invalid_context_rejected_before_process(analysis, kind): + """Invalid identities and byte/case limits cannot become valid measurements.""" + source, index, body = SOURCE, 0, b"input" + if kind == "source-short": + source = "a" * 63 + elif kind == "source-uppercase": + source = SOURCE.upper() + elif kind == "case-bool": + index = True + elif kind == "case-negative": + index = -1 + elif kind == "case-large": + index = 64 + else: + body = b"x" * 32769 + with pytest.raises((ValueError, OSError)): + bridge.make_request(analysis, source, index, body) + assert not list(analysis.glob("csharp-request-*.json")) + + +def test_forced_request_name_collision_preserves_original(monkeypatch, analysis): + """Even a forced random-name collision must fail rather than overwrite approved context.""" + monkeypatch.setattr(bridge.secrets, "token_hex", lambda count: "a" * (count * 2)) + path, _value = request(analysis) + original = path.read_bytes() + with pytest.raises((ValueError, OSError)): + request(analysis, b"different", 1) + assert path.read_bytes() == original + + +@pytest.mark.parametrize("exit_code", [0, 2, 7, 255]) +def test_actual_inert_child_exit_and_binary_stdin_are_preserved(analysis, exit_code): + """Actual child exits differ from the bridge's successful collection exit of zero.""" + body = "\x00first\n\U0001f680last\n".encode() + witness = analysis / "child-input.bin" + bridge.ASSEMBLY.write_text( + "import pathlib,sys\n" + f"pathlib.Path({str(witness)!r}).write_bytes(sys.stdin.buffer.read())\n" + f"sys.exit({exit_code})\n", + encoding="utf-8", + ) + path, value = request(analysis, body) + assert bridge.observe(path, io.BytesIO(body)) == 0 + assert witness.read_bytes() == body + observed = bridge.read_observation(path, value) + assert observed["target_exit_code"] == exit_code and type(observed["target_exit_code"]) is int + assert observed["target"] == str(bridge.ASSEMBLY) + assert observed["bridge_sha256"] == hashlib.sha256(Path(bridge.__file__).read_bytes()).hexdigest() + assert {key: observed[key] for key in value if key != "receipt"} == { + key: val for key, val in value.items() if key != "receipt" + } + + +@pytest.mark.skipif(os.name != "posix", reason="Negative signal return codes are POSIX subprocess semantics") +def test_actual_inert_signal_exit_is_not_normalized_to_collector_exit(analysis): + """An actual SIGTERM result remains -15 instead of being confused with bridge zero.""" + bridge.ASSEMBLY.write_text("import os,signal\nos.kill(os.getpid(), signal.SIGTERM)\n", encoding="utf-8") + path, value = request(analysis) + assert bridge.observe(path, io.BytesIO(b"approved stdin")) == 0 + assert bridge.read_observation(path, value)["target_exit_code"] == -15 + + +def test_receipt_is_absent_until_process_wait_returns_and_argv_has_no_shell(monkeypatch, analysis): + """Spy on the real call boundary, independently checking no premature completion receipt.""" + body = b"$(not-a-shell); & literal input" + path, value = request(analysis, body) + receipt = analysis / value["receipt"] + calls = [] + + def run(argv, **kwargs): + assert not receipt.exists() + assert argv == [str(bridge.DOTNET), str(bridge.ASSEMBLY)] + assert kwargs["input"] == body and kwargs["check"] is False + assert not kwargs.get("shell", False) and not kwargs.get("capture_output", False) + assert kwargs.get("stdout") is None and kwargs.get("stderr") is None + calls.append(argv) + return subprocess.CompletedProcess(argv, 7) + + monkeypatch.setattr(bridge.subprocess, "run", run) + assert bridge.observe(path, io.BytesIO(body)) == 0 and len(calls) == 1 + assert bridge.read_observation(path, value)["target_exit_code"] == 7 + + +def test_changed_stdin_never_starts_target(monkeypatch, analysis): + """Same-length altered bytes cannot borrow a request's approved input hash.""" + path, value = request(analysis, b"expected") + monkeypatch.setattr( + bridge.subprocess, "run", lambda *args, **kwargs: pytest.fail("Target must not start") + ) + with pytest.raises((ValueError, OSError)): + bridge.observe(path, io.BytesIO(b"modified")) + assert not (analysis / value["receipt"]).exists() + + +def test_request_changed_during_target_cannot_publish_completion(monkeypatch, analysis): + """A completed child cannot validate a context that changed while it was running.""" + path, value = request(analysis) + + def run(argv, **kwargs): + altered = {**value, "case_index": 1} + path.write_text(json.dumps(altered), encoding="utf-8") + return subprocess.CompletedProcess(argv, 0) + + monkeypatch.setattr(bridge.subprocess, "run", run) + with pytest.raises((ValueError, OSError)): + bridge.observe(path, io.BytesIO(b"approved stdin")) + assert not (analysis / value["receipt"]).exists() + + +def test_missing_observation_never_looks_like_zero_exit(analysis): + """A successful request is not itself proof of completion.""" + path, value = request(analysis) + with pytest.raises((ValueError, OSError)): + bridge.read_observation(path, value) + + +@pytest.mark.parametrize( + "defect", + [ + "schema-bool", + "bytes-bool", + "nonce", + "receipt-traversal", + "unknown", + "duplicate", + "truncated", + "nonfinite", + ], +) +def test_malformed_request_cannot_launch_the_target(monkeypatch, analysis, defect): + """Treat the serialized request as an untrusted protocol, including duplicate/nonfinite JSON.""" + path, value = request(analysis) + altered = copy.deepcopy(value) + changes = { + "schema-bool": ("schema", True), + "bytes-bool": ("stdin_bytes", True), + "nonce": ("nonce", "bad"), + "receipt-traversal": ("receipt", "../outside.json"), + "unknown": ("unexpected", "field"), + } + if defect in changes: + field, changed = changes[defect] + altered[field] = changed + content = json.dumps(altered).encode() + elif defect == "duplicate": + content = json.dumps(altered).encode()[:-1] + b',"schema":1}' + elif defect == "nonfinite": + content = json.dumps(altered).replace('"case_index": 0', '"case_index": NaN').encode() + else: + content = b'{"schema":' + path.write_bytes(content) + monkeypatch.setattr( + bridge.subprocess, "run", lambda *args, **kwargs: pytest.fail("Malformed request must not execute") + ) + with pytest.raises((ValueError, OSError)): + bridge.observe(path, io.BytesIO(b"approved stdin")) + assert not (analysis / value["receipt"]).exists() + + +def test_existing_receipt_cannot_be_replaced_or_execute_again(monkeypatch, analysis): + """Repeat collection refuses stale proof and preserves its original bytes.""" + path, value, receipt = stub_observation(monkeypatch, analysis, exit_code=2) + original = receipt.read_bytes() + monkeypatch.setattr( + bridge.subprocess, "run", lambda *args, **kwargs: pytest.fail("Stale receipt must prevent execution") + ) + with pytest.raises((ValueError, OSError)): + bridge.observe(path, io.BytesIO(b"approved stdin")) + assert receipt.read_bytes() == original and bridge.read_observation(path, value)["target_exit_code"] == 2 + + +@pytest.mark.parametrize( + "defect", + [ + "nonce", + "case", + "source", + "stdin", + "bytes", + "target", + "bridge", + "exit-bool", + "exit-high", + "exit-low", + "unknown", + "duplicate", + "nonfinite", + "truncated", + "oversize", + ], +) +def test_observation_tamper_and_malformed_bytes_rejected(monkeypatch, analysis, defect): + """Alter real published receipt bytes; matching filenames alone never establish completion.""" + path, value, receipt = stub_observation(monkeypatch, analysis) + observed = json.loads(receipt.read_bytes()) + changes = { + "nonce": ("nonce", "c" * 64), + "case": ("case_index", 1), + "source": ("source_sha256", "c" * 64), + "stdin": ("stdin_sha256", "c" * 64), + "bytes": ("stdin_bytes", 0), + "target": ("target", "/untrusted/other.dll"), + "bridge": ("bridge_sha256", "c" * 64), + "exit-bool": ("target_exit_code", True), + "exit-high": ("target_exit_code", 256), + "exit-low": ("target_exit_code", -129), + "unknown": ("extra", 1), + } + if defect in changes: + key, changed = changes[defect] + observed[key] = changed + body = json.dumps(observed).encode() + elif defect == "duplicate": + body = json.dumps(observed).encode()[:-1] + b',"schema":1}' + elif defect == "nonfinite": + body = json.dumps(observed).replace('"target_exit_code": 0', '"target_exit_code": NaN').encode() + elif defect == "truncated": + body = b'{"schema":1' + else: + body = b" " * 65537 + receipt.write_bytes(body) + with pytest.raises((ValueError, OSError)): + bridge.read_observation(path, value) + + +def test_reader_rejects_changed_expected_request(monkeypatch, analysis): + """The caller's source/case identity must agree with the immutable on-disk request too.""" + path, value, _receipt = stub_observation(monkeypatch, analysis) + changed = copy.deepcopy(value) + changed["source_sha256"] = "d" * 64 + with pytest.raises((ValueError, OSError)): + bridge.read_observation(path, changed) + + +@pytest.mark.parametrize("kind", ["request-hardlink", "receipt-hardlink", "receipt-symlink"]) +def test_aliasing_paths_are_not_accepted_as_owned_regular_receipts(monkeypatch, analysis, kind): + """Independent filesystem aliases must not bypass the descriptor/link-count contract.""" + path, value, receipt = stub_observation(monkeypatch, analysis) + selected = path if kind == "request-hardlink" else receipt + alias = analysis / "alias.json" + if kind.endswith("hardlink"): + alias.hardlink_to(selected) + else: + original = receipt.read_bytes() + receipt.unlink() + alias.write_bytes(original) + try: + receipt.symlink_to(alias) + except OSError: + pytest.skip("Symlink creation unavailable for this Windows test account") + with pytest.raises((ValueError, OSError)): + bridge.read_observation(path, value) diff --git a/tests/test_csharp_measurement.py b/tests/test_csharp_measurement.py new file mode 100644 index 0000000..d99c475 --- /dev/null +++ b/tests/test_csharp_measurement.py @@ -0,0 +1,160 @@ +# +# Generated definition map; @L is the exact declaration line. +# Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. +# functions: measurement(monkeypatch, tmp_path, actual_exit, defect)@L31; target(argv)@L49; execute(argv, stdin, timeout, env)@L57; test_real_bridge_observation_keeps_target_nonzero_separate_from_collector(monkeypatch, tmp_path, actual_exit, expected_status)@L111; test_native_measurement_cannot_promote_bad_collection_to_coverage(monkeypatch, tmp_path, defect)@L147 +# variables: project@L33; work@L38; root@L40; observed@L46; target_exits@L46; launched@L47; result@L58; destination@L61; target_args@L68; request_path@L70; request@L71; index@L72; receipt@L77; content@L79; output@L82; coverage@L105; unused@L105; coverage@L115; observed@L115; project@L115; target_exits@L115; row@L119; row@L120; file@L122; case@L124; index@L124; _project@L149; coverage@L149; observed@L149; target_exits@L149 +# parameters: actual_exit@L31; defect@L31; monkeypatch@L31; tmp_path@L31; _args@L44; argv@L49; kwargs@L49; argv@L57; env@L57; stdin@L57; timeout@L57; actual_exit@L112; expected_status@L112; monkeypatch@L112; tmp_path@L112; defect@L147; monkeypatch@L147; tmp_path@L147 +# + +"""Jason Stys: C# collector/target/report integration without Docker, .NET or source execution. + +The actual native_checks and bridge request/observation protocol run against real +temporary files. Only the compiler, collector process and unused-tool boundary +are inert doubles; hosted CI separately proves real instrumented C# behavior. +Symbol and variable locations are maintained in docs/code-index.md. +""" + +import io +import json +import shlex +import subprocess +import sys +from pathlib import Path + +import pytest + +from examples.projects import fixture +from runner import csharp_coverage_bridge as bridge +from runner import language_checks + + +def measurement(monkeypatch, tmp_path, *, actual_exit=2, defect=None): + """Drive two independently authored cases through actual measurement and receipt readers.""" + project = fixture("csharp").model_dump() + project["cases"] = [ + {"name": "invalid-input", "stdin": "bad \U0001f680\n", "stdout": "", "exit_code": 2}, + {"name": "valid-input", "stdin": "2\n", "stdout": "4", "exit_code": 0}, + ] + work = tmp_path / "project" + work.mkdir() + root = work / ".analysis" + monkeypatch.setattr(bridge, "ANALYSIS", root) + monkeypatch.setattr(bridge, "ASSEMBLY", root / "csharp" / "App.dll") + monkeypatch.setattr( + language_checks, "unused_checks", lambda *_args: {"status": "completed", "findings": []} + ) + observed, target_exits = [], [] + launched = {"exit": None} + + def target(argv, **kwargs): + assert argv == [str(bridge.DOTNET), str(bridge.ASSEMBLY)] + assert kwargs["check"] is False and type(kwargs["input"]) is bytes + target_exits.append(launched["exit"]) + return subprocess.CompletedProcess(argv, launched["exit"]) + + monkeypatch.setattr(bridge.subprocess, "run", target) + + def execute(argv, stdin="", *, timeout, env=None): + result = {"exit_code": 0, "stdout": "", "stderr": "", "timeout": False, "output_truncated": False} + if argv[0] == "dotnet": + assert argv[1] == "build" and timeout == 45 + destination = Path(argv[argv.index("-o") + 1]) + destination.mkdir() + (destination / "App.dll").write_bytes(b"inert assembly marker; never executed") + (destination / "App.pdb").write_bytes(b"inert symbols marker") + return result + assert argv[0] == "/opt/coverlet/coverlet" and timeout == 20 and env == {} + assert argv[argv.index("--target") + 1] == sys.executable + target_args = shlex.split(argv[argv.index("--targetargs") + 1]) + assert target_args[:2] == ["-I", "/opt/csharp_coverage_bridge.py"] and len(target_args) == 3 + request_path = Path(target_args[2]) + request = json.loads(request_path.read_bytes()) + index = len(observed) + assert request["case_index"] == index and stdin == project["cases"][index]["stdin"] + launched["exit"] = actual_exit if index == 0 else 0 + if defect != "missing-receipt": + assert bridge.observe(request_path, io.BytesIO(stdin.encode())) == 0 + receipt = root / request["receipt"] + if defect == "binding-tamper": + content = json.loads(receipt.read_bytes()) + content["source_sha256"] = "f" * 64 + receipt.write_text(json.dumps(content), encoding="utf-8") + output = Path(argv[argv.index("--output") + 1]).with_suffix(".cobertura.xml") + if defect != "missing-xml": + output.write_text( + "" + if defect == "bad-xml" + else '' + '' + "", + encoding="utf-8", + ) + observed.append( + {"request_path": request_path, "request": request, "receipt": receipt, "output": output} + ) + if defect == "collector-error": + result["exit_code"] = 1 + elif defect == "collector-missing-exit": + result["exit_code"] = None + elif defect == "timeout": + result["timeout"] = True + elif defect == "truncated": + result["output_truncated"] = True + return result + + coverage, unused = language_checks.native_checks(project, work, execute, ["dotnet", "compiled/App.dll"]) + assert unused == {"status": "completed", "findings": []} + return coverage, observed, target_exits, project + + +@pytest.mark.parametrize("actual_exit, expected_status", [(2, "measured"), (7, "partial")]) +def test_real_bridge_observation_keeps_target_nonzero_separate_from_collector( + monkeypatch, tmp_path, actual_exit, expected_status +): + """Only matching real observed target returns can complete the approved case set.""" + coverage, observed, target_exits, project = measurement(monkeypatch, tmp_path, actual_exit=actual_exit) + assert coverage["status"] == expected_status and coverage["requested_cases"] == 2 + assert target_exits == [actual_exit, 0] + assert len(observed) == len(coverage["cases"]) == 2 + assert len({row["request"]["nonce"] for row in observed}) == 2 + assert len({row["receipt"] for row in observed}) == 2 + assert coverage["tool"] == "Coverlet 8.0.1" + file = coverage["files"][project["entry"]] + assert file["status"] == "measured" and file["executed_lines"] == [2] and file["missing_lines"] == [3] + for index, case in enumerate(coverage["cases"]): + assert case["name"] == project["cases"][index]["name"] + assert case["expected_exit_code"] == project["cases"][index]["exit_code"] + assert case["exit_code"] == case["target_exit_code"] == target_exits[index] + assert case["collector_exit_code"] == 0 and case["collector_output_truncated"] is False + assert case["bridge"] == bridge.read_observation( + observed[index]["request_path"], observed[index]["request"] + ) + + +@pytest.mark.parametrize( + "defect", + [ + "collector-error", + "collector-missing-exit", + "timeout", + "truncated", + "missing-receipt", + "binding-tamper", + "missing-xml", + "bad-xml", + ], +) +def test_native_measurement_cannot_promote_bad_collection_to_coverage(monkeypatch, tmp_path, defect): + """Valid target proof cannot mask collector failure; valid XML cannot replace target proof.""" + coverage, observed, target_exits, _project = measurement(monkeypatch, tmp_path, defect=defect) + assert coverage["status"] == "blocked" and coverage["reason"].startswith("Native coverage unavailable:") + assert len(observed) == 1 + if defect in {"collector-error", "collector-missing-exit", "timeout", "truncated"}: + # The negative intentionally has BOTH valid target proof and XML. + assert target_exits == [2] and observed[0]["output"].exists() + assert ( + bridge.read_observation(observed[0]["request_path"], observed[0]["request"])["target_exit_code"] + == 2 + ) + elif defect == "missing-receipt": + assert target_exits == [] and observed[0]["output"].exists() and not observed[0]["receipt"].exists() diff --git a/tests/test_release.py b/tests/test_release.py index f9925ec..5871d68 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -23,10 +23,10 @@ @pytest.mark.parametrize( "name", [ - "Code Flow Studio Setup 0.4.2.exe", - "Code Flow Studio-0.4.2-arm64.dmg", - "Code Flow Studio-0.4.2.AppImage", - "CodeFlowStudio-0.4.2-windows-x64.zip", + "Code Flow Studio Setup 0.4.3.exe", + "Code Flow Studio-0.4.3-arm64.dmg", + "Code Flow Studio-0.4.3.AppImage", + "CodeFlowStudio-0.4.3-windows-x64.zip", ], ) def test_checksum_download_filename(tmp_path, name): diff --git a/tests/test_release_gate.py b/tests/test_release_gate.py index 73ef792..3ee17fe 100644 --- a/tests/test_release_gate.py +++ b/tests/test_release_gate.py @@ -695,7 +695,7 @@ def test_dirty_source_and_publish_operation_are_refused_before_network(tmp_path, def test_actual_checked_in_product_version_and_worker_metadata(): """Exercise the production data-only coherence reader independently of prepared-context fixtures.""" - assert gate.product_version() == "0.4.2" + assert gate.product_version() == "0.4.3" def test_api_structured_body_and_fixed_namespace(monkeypatch): diff --git a/tests/test_released_service_probe.py b/tests/test_released_service_probe.py index cebbcda..6744120 100644 --- a/tests/test_released_service_probe.py +++ b/tests/test_released_service_probe.py @@ -1,9 +1,9 @@ # # Generated definition map; @L is the exact declaration line. # Function/class docs explain behavior; variables/parameters are scoped inputs, outputs or state. -# functions: test_hosted_guard_refuses_user_pc_before_any_work(monkeypatch, tmp_path)@L27; test_hosted_output_is_confined_and_verifier_source_explicit(monkeypatch, tmp_path)@L36; metadata()@L56; test_publication_or_replaced_asset_never_authorizes_execution(defect)@L95; test_download_binds_canonical_hosts_actual_bytes_and_digest(tmp_path)@L117; transport(request)@L129; archive(path, monkeypatch, extra, bad_schema)@L153; test_real_tar_provenance_and_no_setuid_preservation(monkeypatch, tmp_path)@L180; test_actual_tar_negative_members_fail_closed(monkeypatch, tmp_path, kind)@L194; test_service_environment_cannot_inherit_provider_cloud_or_github_keys(monkeypatch, tmp_path)@L219; test_failed_startup_cleanup_targets_only_owned_child_and_preserves_live_recovery(tmp_path, can_exit)@L230; terminate()@L238; wait(timeout)@L241; test_actual_store_timeline_identity_and_shape_fail_closed(tmp_path, defect)@L255; test_incremental_receipt_and_full_restored_export_equality(monkeypatch, tmp_path)@L283; request(_client, method, path, body)@L291; validate(result, project)@L331 -# variables: name@L39; value@L39; prefix@L58; rows@L59; i@L77; rows@L77; release@L97; tag@L97; body@L119; initial@L120; row@L121; seen@L127; client@L143; digest@L144; worker@L155; value@L157; output@L165; body@L166; name@L166; member@L171; content@L176; member@L176; source@L181; service@L183; source@L195; member@L196; content@L199; content@L204; result@L223; key@L225; temporary@L232; calls@L235; state@L236; process@L248; store@L257; revision@L258; identifier@L259; result@L263; exported@L264; timeline@L266; data@L285; store@L287; validations@L288; tamper@L289; revision@L300; identifier@L301; project@L302; result@L303; identifier@L311; result@L312; evidence@L337; service@L338; private@L339; row@L341; text@L342 -# parameters: monkeypatch@L27; tmp_path@L27; monkeypatch@L36; tmp_path@L36; defect@L95; tmp_path@L117; request@L129; bad_schema@L153; extra@L153; monkeypatch@L153; path@L153; monkeypatch@L180; tmp_path@L180; kind@L194; monkeypatch@L194; tmp_path@L194; monkeypatch@L219; tmp_path@L219; can_exit@L230; tmp_path@L230; timeout@L241; defect@L255; tmp_path@L255; monkeypatch@L283; tmp_path@L283; _client@L291; body@L291; method@L291; path@L291; project@L331; result@L331 +# functions: test_hosted_guard_refuses_user_pc_before_any_work(monkeypatch, tmp_path)@L27; test_hosted_output_is_confined_and_verifier_source_explicit(monkeypatch, tmp_path)@L36; metadata()@L56; test_publication_or_replaced_asset_never_authorizes_execution(defect)@L95; test_download_binds_canonical_hosts_actual_bytes_and_digest(tmp_path)@L117; transport(request)@L129; archive(path, monkeypatch, extra, bad_schema)@L153; test_real_tar_provenance_and_no_setuid_preservation(monkeypatch, tmp_path)@L180; test_actual_tar_negative_members_fail_closed(monkeypatch, tmp_path, kind)@L194; test_service_environment_cannot_inherit_provider_cloud_or_github_keys(monkeypatch, tmp_path)@L219; test_failed_startup_cleanup_targets_only_owned_child_and_preserves_live_recovery(tmp_path, can_exit)@L230; terminate()@L238; wait(timeout)@L241; test_actual_store_timeline_identity_and_shape_fail_closed(tmp_path, defect)@L255; test_incremental_receipt_and_full_restored_export_equality(monkeypatch, tmp_path)@L283; request(_client, method, path, body)@L291; validate(result, project, historical_source)@L331 +# variables: name@L39; value@L39; prefix@L58; rows@L59; i@L77; rows@L77; release@L97; tag@L97; body@L119; initial@L120; row@L121; seen@L127; client@L143; digest@L144; worker@L155; value@L157; output@L165; body@L166; name@L166; member@L171; content@L176; member@L176; source@L181; service@L183; source@L195; member@L196; content@L199; content@L204; result@L223; key@L225; temporary@L232; calls@L235; state@L236; process@L248; store@L257; revision@L258; identifier@L259; result@L263; exported@L264; timeline@L266; data@L285; store@L287; validations@L288; tamper@L289; revision@L300; identifier@L301; project@L302; result@L303; identifier@L311; result@L312; evidence@L338; service@L339; private@L340; row@L342; text@L343 +# parameters: monkeypatch@L27; tmp_path@L27; monkeypatch@L36; tmp_path@L36; defect@L95; tmp_path@L117; request@L129; bad_schema@L153; extra@L153; monkeypatch@L153; path@L153; monkeypatch@L180; tmp_path@L180; kind@L194; monkeypatch@L194; tmp_path@L194; monkeypatch@L219; tmp_path@L219; can_exit@L230; tmp_path@L230; timeout@L241; defect@L255; tmp_path@L255; monkeypatch@L283; tmp_path@L283; _client@L291; body@L291; method@L291; path@L291; historical_source@L331; project@L331; result@L331 # """Jason Stys: published-package/ownership/protocol negatives; no local Docker, network or service runs.""" @@ -328,7 +328,8 @@ def request(_client, method, path, body=None): return {"saved": True} raise AssertionError("Unexpected route") - def validate(result, project): + def validate(result, project, *, historical_source=None): + assert historical_source == probe.SOURCE assert result["compilation"]["exit_code"] == 0 and result["checks"]["passed"] == len(project.cases) validations.append(project.language) diff --git a/ui/App.tsx b/ui/App.tsx index 2a8ac5b..380577a 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -439,7 +439,7 @@ export default function App({
- ENGINEERING WORKBENCH / v0.4.2 + ENGINEERING WORKBENCH / v0.4.3

{tab === "workspace" ? "Make your next iteration count."