codemetrics-ai 0.4.0 → 0.6.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +51 -5
  2. package/dist/analyzer.d.ts +1 -0
  3. package/dist/analyzer.js +11 -5
  4. package/dist/cli.js +7 -3
  5. package/dist/dependency-evidence.d.ts +5 -0
  6. package/dist/dependency-evidence.js +30 -0
  7. package/dist/dependency-inventory.d.ts +67 -0
  8. package/dist/dependency-inventory.js +203 -0
  9. package/dist/dependency-metadata.d.ts +65 -0
  10. package/dist/dependency-metadata.js +125 -0
  11. package/dist/dependency-pnpm.d.ts +4 -0
  12. package/dist/dependency-pnpm.js +237 -0
  13. package/dist/dependency-spec.d.ts +13 -0
  14. package/dist/dependency-spec.js +30 -0
  15. package/dist/error-handling-scoring.d.ts +1 -0
  16. package/dist/error-handling-scoring.js +15 -0
  17. package/dist/error-handling.d.ts +4 -1
  18. package/dist/error-handling.js +50 -17
  19. package/dist/evidence-reader.js +3 -1
  20. package/dist/evidence-tools.js +4 -0
  21. package/dist/failure-contracts.d.ts +19 -0
  22. package/dist/failure-contracts.js +261 -0
  23. package/dist/function-scoring.d.ts +2 -1
  24. package/dist/function-scoring.js +13 -24
  25. package/dist/handler-disposition.d.ts +9 -0
  26. package/dist/handler-disposition.js +156 -0
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +1 -0
  29. package/dist/python/ASYNC_ADAPTERS.md +38 -0
  30. package/dist/python/ASYNC_CONTEXT.md +58 -0
  31. package/dist/python/ASYNC_SCORING.md +174 -0
  32. package/dist/python/ASYNC_USAGE.md +62 -0
  33. package/dist/python/BEHAVIOR_REVIEW.md +84 -0
  34. package/dist/python/COVERAGE.md +96 -0
  35. package/dist/python/DECOMPOSITION.md +46 -0
  36. package/dist/python/DUPLICATION.md +36 -0
  37. package/dist/python/ERROR_HANDLING.md +45 -0
  38. package/dist/python/ERROR_HANDLING_CONTEXT.md +61 -0
  39. package/dist/python/ERROR_HANDLING_OPERATIONS.md +54 -0
  40. package/dist/python/ERROR_HANDLING_TRANSFERS.md +64 -0
  41. package/dist/python/METHOD_COMPLEXITY.md +46 -0
  42. package/dist/python/README.md +162 -0
  43. package/dist/python/analysis.py +54 -0
  44. package/dist/python/async_adapters.py +243 -0
  45. package/dist/python/async_bindings.py +180 -0
  46. package/dist/python/async_context.py +143 -0
  47. package/dist/python/async_helper_paths.py +186 -0
  48. package/dist/python/async_provenance.py +129 -0
  49. package/dist/python/async_scoring.py +203 -0
  50. package/dist/python/async_usage.py +244 -0
  51. package/dist/python/coverage_collect.py +121 -0
  52. package/dist/python/coverage_provenance.py +77 -0
  53. package/dist/python/coverage_review.py +273 -0
  54. package/dist/python/coverage_worker.py +46 -0
  55. package/dist/python/decomposition.py +199 -0
  56. package/dist/python/duplication.py +177 -0
  57. package/dist/python/error_handling.py +249 -0
  58. package/dist/python/error_handling_context.py +294 -0
  59. package/dist/python/error_handling_operations.py +228 -0
  60. package/dist/python/error_handling_transfers.py +211 -0
  61. package/dist/python/review_cli.py +36 -0
  62. package/dist/python/source_facts.py +293 -0
  63. package/dist/python/source_selection.py +157 -0
  64. package/dist/python-analyzer.d.ts +21 -0
  65. package/dist/python-analyzer.js +123 -0
  66. package/dist/python-async-adapters.d.ts +67 -0
  67. package/dist/python-async-adapters.js +197 -0
  68. package/dist/python-async-context.d.ts +73 -0
  69. package/dist/python-async-context.js +181 -0
  70. package/dist/python-async-scoring.d.ts +89 -0
  71. package/dist/python-async-scoring.js +249 -0
  72. package/dist/python-async-usage.d.ts +56 -0
  73. package/dist/python-async-usage.js +143 -0
  74. package/dist/python-cli.d.ts +2 -0
  75. package/dist/python-cli.js +53 -0
  76. package/dist/python-collect-cli.d.ts +2 -0
  77. package/dist/python-collect-cli.js +28 -0
  78. package/dist/python-complexity.d.ts +18 -0
  79. package/dist/python-complexity.js +36 -0
  80. package/dist/python-coverage.d.ts +63 -0
  81. package/dist/python-coverage.js +173 -0
  82. package/dist/python-decomposition.d.ts +56 -0
  83. package/dist/python-decomposition.js +102 -0
  84. package/dist/python-duplication.d.ts +65 -0
  85. package/dist/python-duplication.js +89 -0
  86. package/dist/python-error-context.d.ts +84 -0
  87. package/dist/python-error-context.js +165 -0
  88. package/dist/python-error-handling.d.ts +56 -0
  89. package/dist/python-error-handling.js +120 -0
  90. package/dist/python-error-operations.d.ts +44 -0
  91. package/dist/python-error-operations.js +155 -0
  92. package/dist/python-error-transfers.d.ts +38 -0
  93. package/dist/python-error-transfers.js +125 -0
  94. package/dist/python-evidence.d.ts +23 -0
  95. package/dist/python-evidence.js +255 -0
  96. package/dist/score-arithmetic.d.ts +1 -12
  97. package/dist/score-arithmetic.js +1 -40
  98. package/dist/shared-source-facts/arithmetic.d.mts +9 -0
  99. package/dist/shared-source-facts/arithmetic.mjs +32 -0
  100. package/dist/shared-source-facts/contract.d.mts +35 -0
  101. package/dist/shared-source-facts/contract.mjs +95 -0
  102. package/dist/shared-source-facts/population.d.mts +12 -0
  103. package/dist/shared-source-facts/population.mjs +41 -0
  104. package/dist/source-facts.d.ts +4 -0
  105. package/dist/source-facts.js +16 -0
  106. package/package.json +15 -6
@@ -0,0 +1,58 @@
1
+ # Python async call context
2
+
3
+ `async_context.py` implements the policy `python/async-call-context-review-v1`. It supplements the existing [async usage report](ASYNC_USAGE.md) with an unresolved-call inventory and bounded source links. **Async/Blocking Usage and Error Handling remain unscored.** Method Complexity remains the sole scored Python dimension.
4
+
5
+ Development adapter 0.13.0 embeds this report at `dimensions.performanceAsync.contextReview` under ruleset `python-2026-10-18-async-context-review-v1`. The npm archive ships both producers and this guide; the companion skill presents the context without scores. Historical 0.12.0 evidence retains its original contract without invented context observations. The original async rows, categories, counts, ownership, and configuration remain unchanged.
6
+
7
+ ## Run
8
+
9
+ ```powershell
10
+ py -3.12 analyzers/python/async_context.py E:/repos/example --output E:/temp/async-context.json
11
+ py -3.12 analyzers/python/async_context_corpus.py --repos E:/repos --output TestResults/python-async-context-new
12
+ ```
13
+
14
+ The same source-selection flags and optional run/audit UUIDs as the async reviewer are supported. Output must be a new `.json` file in an existing directory; the corpus runner requires a new output directory. Target source is parsed, never imported, installed, built, or executed.
15
+
16
+ ## Inventory and accounting
17
+
18
+ Each call retains its original full-extent call ID. The report records its callee shape: bare name, attribute path, attribute on an expression, call result, subscript, lambda, or other expression. Unresolved import-match reasons come unchanged from the original report. A joint reason/shape inventory includes expression counts and up to three deterministic owner/call references per group.
19
+
20
+ Direct async-body and deferred-generator-body observations remain separate, as do production, test, support, and generated populations. Synchronous functions and lambdas remain excluded. A population with no async owners is unavailable, with null context measurements.
21
+
22
+ Unresolved import counts are not reduced by local source links. For example, finding a local function definition does not establish that its implementation is nonblocking. Names such as `read`, `wait`, or `get` do not imply blocking. Bare-name groups also contain builtins and classes; shape is not inferred callable type.
23
+
24
+ ## Local callback and callee links
25
+
26
+ A bare name may link to a selected function definition in its lexical scope or an enclosing lexical scope in the same file. The definition must be a unique direct-suite binding, undecorated, and end before the reference in source order. Parameter shadowing, duplicate/reassigned names, conditional definitions, comprehension-local bindings, reflection, and cross-scope writes withhold links. Class namespaces are not treated as lexical closures for methods.
27
+
28
+ Links identify source-fact owner IDs, source extents, member names, and `FunctionDef` versus `AsyncFunctionDef`. No callback body is traversed or charged to the caller. A link to an async callback is not a claim that passing it to a thread helper is valid. Imported callback matches remain import spellings, not authenticated external implementations.
29
+
30
+ Helper callbacks distinguish reference expressions from eager call expressions. `to_thread(work())` retains the eager `work()` call under its real async owner; it does not gain a callback-definition link for the returned value. Lambdas retain their separate synchronous owner. Receiver methods, callable objects, arbitrary aliases, re-exports, partial application, and external modules are not resolved.
31
+
32
+ ## Stored wrappers
33
+
34
+ The narrow supported pattern is a same-async-owner, direct-suite, single-name assignment of an import-matched `asgiref.sync.sync_to_async` call, followed by a call through that name:
35
+
36
+ ```python
37
+ async def run(value):
38
+ def work(item):
39
+ return item + 1
40
+ wrapped = sync_to_async(work)
41
+ return await wrapped(value)
42
+ ```
43
+
44
+ The wrapper name must have exactly one binding across the owner. Construction must end before use, with a non-starred first positional callback. Conditional, annotated, chained, destructured, reassigned, deleted, parameter-shadowed, module-level, and enclosing-owner wrapper bindings are withheld. Deferred generator-body invocations are also withheld. An eager first generator iterable retains the enclosing context. Existing imported-module mutation and reflective/cross-scope vetoes apply.
45
+
46
+ Each supported invocation links to the original construction call ID and assignment extent. The callback reference is interpreted at construction, not in a use-site comprehension's local namespace. Repeated uses link to the same construction; construction and invocation must not be added together as two offloaded operations. Local-callback summary counts are reference observations and can include the same callback on construction and invocation rows; they are neither distinct callbacks nor execution counts.
47
+
48
+ Only `sync_to_async` stored wrapper syntax is followed. Thread-helper callback references are described, but returned awaitables, stored `to_thread` calls, decorated wrappers, `functools.partial`, and `loop.run_in_executor` receiver types are not inferred.
49
+
50
+ ## Provenance and failure behavior
51
+
52
+ The report shares the source async run/audit IDs and records a hash of the original async rows, source selection, and source-facts producer/policy. Before collection it regenerates and compares the original report, ignoring only its generation timestamp. During collection it rereads source digests and checks that every original owner and owned async call measurement is unchanged. The existing 100,000-owner and 100,000-call budgets apply.
53
+
54
+ Incomplete, stale, tampered, or over-budget inputs exit 2 with diagnostics, empty rows, and a null summary. These checks do not authenticate reports, lock a changing filesystem, prove control flow, or establish runtime identity. Unique lexical syntax can still be affected by runtime mutation outside the analysis boundary.
55
+
56
+ No score, finding, deduction, gate, automatic concurrency recommendation, or judgment of documented business intent is emitted. Source links provide review context only.
57
+
58
+ The [pinned study](../../shared/scorecard-schema/calibration-runs/python-async-context-2026-10-17.md) records real-project observations, reviewed examples, unchanged prior evidence, and test results.
@@ -0,0 +1,174 @@
1
+ # Python Async/Blocking Usage scoring study
2
+
3
+ Policy `python/async-blocking-owner-rate-v1` implements a standalone numeric score
4
+ over the existing [async helper evidence](BEHAVIOR_REVIEW.md). It measures detected
5
+ blocking usage in selected production async implementations. It does not measure
6
+ latency, throughput, appropriate concurrency, or the correctness of a business
7
+ decision.
8
+
9
+ Python adapter **0.15.0**, ruleset `python-2026-10-28-async-scoring-v1`, now packages
10
+ this policy at `dimensions.performanceAsync`, with `helperReview` and `asyncScoring`
11
+ reports supporting the dimension score. The standalone report keeps its study
12
+ producer and scope; the enclosing dimension supplies the versioned scorecard
13
+ contract. Method Complexity is unchanged; Error Handling remains unscored.
14
+
15
+ The canonical validator checks identities, source/selection hashes, bounded path
16
+ structure, root-call references, classifications and contribution accounting. It
17
+ recomputes the owner-rate score and rejects inconsistent supplied scores. It does
18
+ not independently reparse Python during historical import or authenticate source
19
+ claims: a coherently rewritten source report is outside those integrity guarantees.
20
+ Source analysis itself regenerates and verifies the measurements before scoring.
21
+
22
+ Current aggregates use only available dimension scores and must be labeled partial
23
+ assessment, at most 2/9 dimensions scored. Historical 0.14.0 and earlier evidence
24
+ keeps its original contract. Compatible 0.15.0 runs can compare/gate; incompatible
25
+ versions, policies, scopes and selections cannot. The npm archive remains a local
26
+ development delivery, not a published Python release.
27
+
28
+ ## Population and ladder
29
+
30
+ Let N be the number of distinct selected production `async def` implementations,
31
+ including async generators. Let B be the number of those owners with at least one
32
+ non-excluded blocking-catalogue observation in its eager execution context.
33
+ Each owner contributes once to B and once to N, regardless of call or path count.
34
+ Ordinary synchronous functions cannot dilute the denominator. Selected test,
35
+ support, and generated owners retain observations but do not enter either count.
36
+ Declaration-only exclusions remain those of source selection. Nested async owners
37
+ count separately; their bodies never count against their enclosing function.
38
+
39
+ | Affected owner rate | Score |
40
+ | --- | ---: |
41
+ | B = 0 or below 1% | 10 |
42
+ | Below 2% | 9 |
43
+ | Below 5% | 8 |
44
+ | At most 10% | 6 |
45
+ | At most 15% | 4 |
46
+ | At most 20% | 2 |
47
+ | Above 20% | 0 |
48
+
49
+ The accepted JS/TS owner-rate bands supply the product-policy baseline. Exact
50
+ integer cross-products choose the band; rounded percentages never choose it.
51
+ Zero eligible owners means **unavailable**, not 10. A small population can score
52
+ 0 for one affected owner. Below 1%, a 10 can still contain scored observations;
53
+ always show B and N. These thresholds are coaching policy, not empirical defect
54
+ prediction or evidence of score equivalence between languages.
55
+
56
+ All selected production async implementations enter N, even if they currently do
57
+ no awaiting or delegate to another async function. This is a declared-async
58
+ population, consistent with the existing Python ownership inventory. It is not a
59
+ measurement of actual suspension, nontrivial workloads, or reachable functions.
60
+ Changing eligibility later requires another policy rather than silent retuning.
61
+
62
+ ## Scored observations and exclusions
63
+
64
+ The seven existing import spellings are the complete initial catalogue:
65
+ `time.sleep`, `subprocess.run`, `subprocess.call`, `subprocess.check_call`,
66
+ `subprocess.check_output`, `os.system`, and `urllib.request.urlopen`.
67
+
68
+ Direct calls and supported eager same-file synchronous helper paths qualify.
69
+ Repeated helper calls affect their async root once. Two async roots using the same
70
+ helper affect two owners; `distinctScoredSourceCalls` separately deduplicates the
71
+ shared blocking expression. Synchronous helpers never become extra denominator
72
+ credit. No severity weighting or additional per-call deduction is applied.
73
+
74
+ Recognized thread-helper callbacks, wrapper callback references, and deferred
75
+ generator work remain observations without deductions. `to_thread(work())`
76
+ evaluates `work()` eagerly and does not receive a callback exemption. A generator
77
+ expression's first iterable retains eager context. Creating a synchronous
78
+ generator or coroutine does not prove execution of its body. Sequential awaits,
79
+ throttling, and missing concurrency are not scored hazards.
80
+
81
+ Import spelling is not authenticated runtime identity. The existing binding and
82
+ helper vetoes apply, including shadowing, mutation, ambiguous decorators, source
83
+ order, same-file scope, and the three-edge helper limit. Arguments and conditions
84
+ are not evaluated; unreachable syntax or zero-duration sleep can remain a signal.
85
+ Receiver calls, cross-file helpers, arbitrary callbacks, CPU-heavy work, and
86
+ non-catalogue APIs remain outside detection. No automatic rewrite is prescribed.
87
+
88
+ ## Documented choice
89
+
90
+ `adjacent-simple-statement-declared-blocking-prose-v1` retains qualifying comments as
91
+ `documented-choice` observations without deductions. It checks each call along
92
+ the eager path, so a rationale at one async call site applies only to that path;
93
+ a rationale on the blocking statement in the shared helper applies to every
94
+ observed path through it.
95
+
96
+ A comment must be an actual Python comment token immediately above a simple
97
+ statement at the same indentation, or trailing its final line. The statement can
98
+ be a call expression, assignment, return, raise, or assertion. Multiple statements
99
+ on a line, one-line compound suites, declaration boundaries, intervening statements,
100
+ blank lines, and comments attached to an enclosing branch do not inherit intent.
101
+ Multiline statements and Python's UTF-8 AST columns are handled explicitly.
102
+
103
+ The prose must contain `blocking`, `synchronous`, `sync-over-async`, or `sleep`,
104
+ and an intent word: `intentional`, `intentionally`, `deliberate`, `deliberately`,
105
+ `required`, `necessary`, or `unavoidable`. It must have at least two alphabetic
106
+ words. Tool directives, TODO/FIXME/HACK/XXX markers, and text that parses as Python
107
+ code do not qualify. Caution words (`not`, `never`, `avoid`, `remove`, `replace`,
108
+ `shouldn't`, `mustn't`, `unnecessary`) withhold automatic exclusion. For example,
109
+ "Blocking should be avoided" is a warning, not an exemption. The conservative
110
+ caution rule can also withhold valid explanations such as "Blocking is required
111
+ because the device does not support async"; rewriting the local declaration to
112
+ "Synchronous operation required by the device protocol" makes intent explicit.
113
+ This is a deterministic documentation convention, not natural-language verification
114
+ that an explanation is adequate. General docstrings and strings that
115
+ look like comments do not qualify. No Python CMAI annotation syntax is introduced
116
+ by this study.
117
+
118
+ ```python
119
+ async def run():
120
+ # Synchronous operation required by the device protocol.
121
+ time.sleep(1)
122
+ ```
123
+
124
+ The observation and rationale remain visible. The score trusts the declared
125
+ choice without judging its business justification or claiming the call is safe.
126
+
127
+ ## Unknowns and availability
128
+
129
+ Unresolved paths remain attached to every owner and summarized by reason, along
130
+ with `ownersWithUnresolvedPaths`. They receive no deduction and are not classified
131
+ as nonblocking or correct. Owners with unresolved paths remain in the declared
132
+ async population; removing them would select a different denominator and could
133
+ inflate or distort the affected rate. These counts are not semantic-analysis
134
+ coverage percentages. Even an imported non-catalogue call can have unknown
135
+ behavior without producing an unresolved helper path.
136
+
137
+ A 10 means the observed affected-owner rate falls in the top band of this narrow
138
+ catalogue. It does **not** certify excellent or safe async code. Report unresolved
139
+ counts and the limited scope beside any score, especially for a zero-match
140
+ codebase. Neither the absence of catalogue matches nor passing curated tests
141
+ establishes recall across real Python applications.
142
+
143
+ An incomplete parse, stale/tampered helper evidence, source mismatch, or exhausted
144
+ global traversal/index budget withholds the score and all contribution rows.
145
+ The existing helper-depth limit retains an explicit unresolved path. The new
146
+ comment/source index is capped at 1,000,000 AST nodes across files needed for
147
+ candidate classification; helper budgets remain unchanged. All prior evidence is
148
+ regenerated against source and compared before scoring. Selection, policies,
149
+ configuration fingerprints, source digests, and run/audit IDs bind the result.
150
+ These integrity checks do not authenticate externally edited files or lock a
151
+ concurrently changing filesystem.
152
+
153
+ ## Run and validate
154
+
155
+ Python 3.12, standard library only; target code is never imported or executed:
156
+
157
+ ```powershell
158
+ py -3.12 analyzers/python/async_scoring.py E:/repos/project --output TestResults/async-score.json
159
+ py -3.12 -m unittest discover -s analyzers/python/tests -p test_async_scoring.py -v
160
+ py -3.12 analyzers/python/async_scoring_corpus.py --repos E:/repos --baseline TestResults/python-async-adapters-package-2026-10-24/final --helpers TestResults/python-behavior-review-final-2026-10-24 --output TestResults/async-scoring-new
161
+ ```
162
+
163
+ The scoring CLI accepts the existing selection flags and optional UUID run/audit
164
+ IDs. It requires a new output JSON file with an existing parent, or writes to
165
+ stdout. Complete reports, including empty populations, exit 0; incomplete reports
166
+ and invalid invocations exit 2. This output is not a v3 scorecard or a gate input.
167
+
168
+ The corpus runner verifies clean pinned revisions before and after each run,
169
+ exact original source facts, and exact prior async/helper evidence apart from
170
+ fresh invocation IDs/timestamps. It records implementation hashes, contribution
171
+ hashes, and uncertainty alongside scores. It does not rerun target tests or alter
172
+ corpus source. Unit tests cover exact thresholds, positive/negative execution
173
+ contexts, intent boundaries, populations, shared-helper deduplication, budgets,
174
+ source drift, tampering, and CLI output preservation.
@@ -0,0 +1,62 @@
1
+ # Python Async/Blocking Usage evidence
2
+
3
+ The standalone `async_usage.py` reviewer implements `python/async-owned-calls-review-v1`, producer `0.1.0-experimental`. It records selected async-function populations, call syntax, conservative import matches, and explicit thread-helper syntax. **Async/Blocking Usage and Error Handling remain unscored.** Method Complexity is still Python's only scored dimension. Development adapter 0.13.0 embeds this report at `dimensions.performanceAsync.review` under ruleset `python-2026-10-18-async-context-review-v1`. The npm archive ships both producers and this guide. The scorecard skill reports the evidence without deductions or an async score. Historical 0.11.0 reports retain their original contract without invented async observations.
4
+
5
+ ## Run
6
+
7
+ ```powershell
8
+ py -3.12 analyzers/python/async_usage.py E:/repos/example --output E:/temp/async-review.json
9
+ py -3.12 analyzers/python/async_usage_corpus.py --repos E:/repos --output TestResults/python-async-corpus-new
10
+ ```
11
+
12
+ The reviewer accepts the existing selection flags (`--include`, `--exclude`, `--test-path`, `--production-path`, and test/generated/support inclusion flags) plus `--run-id` and `--audit-id`. Output must be a new `.json` file in an existing directory. The corpus command requires a new output directory and the pinned clean Starlette/Django checkouts in the selection manifest. Neither command imports, installs, builds, or executes target code.
13
+
14
+ Complete evidence exits 0. Parse/read failures, stale source facts, inconsistent selection, or exhausted budgets exit 2 with incomplete diagnostic evidence, empty function rows, and a null summary. Caller-supplied invocation IDs must be UUIDs; otherwise fresh IDs are generated. Function IDs and source digests come from source facts. Call IDs include the owning function and complete start/end extent, so nested calls sharing a starting position remain distinct. Columns use Python AST's zero-based UTF-8 byte offsets; lines are one-based.
15
+
16
+ ## Population and ownership
17
+
18
+ Every selected implementation owner appears once. Only `AsyncFunctionDef` owners are observed; synchronous functions and lambdas are explicitly excluded rather than counted as clean async implementations. Coroutine and async-generator functions are distinguished. Production, test, support, and generated populations retain separate summaries. An empty async population is unavailable, with null call measurements.
19
+
20
+ A function owns its body, excluding nested function/lambda/class definitions, their headers/defaults/decorators, annotations, type aliases, and class/module initializers. Those exclusions are measurement boundaries, not claims that declaration expressions never execute. Nested async functions are separate owners. No call graph is followed into synchronous helpers or callbacks. `await`, async iteration/context managers, and yielding do not prove suspension, safe blocking behavior, or runtime execution.
21
+
22
+ List/set/dict comprehensions are evaluated within the enclosing context. Generator expressions have separate deferred-body observations; their first iterable expression retains the enclosing execution context. This remains conservative when a generator is immediately consumed: consumption is not inferred. Comprehension iteration bindings shadow import names locally; assignment expressions can affect the enclosing scope. Nested owners never add calls to their parent.
23
+
24
+ ## Import matching
25
+
26
+ `async_bindings.py` recognizes an unambiguous direct-suite import and `as` aliases, followed by a name/attribute path. A matched target is **import spelling**, not proven external runtime identity. Relative imports, conditional imports, repeated/rebound bindings, parameters, assignments, exception targets, pattern captures, and local definitions remain unresolved. An import must precede the use in source order; there is no reaching-definition analysis. Class namespaces are not lexical closures for methods.
27
+
28
+ For this first policy, `global`/`nonlocal` names are vetoed across the file. Direct `exec`, `eval`, `globals`, `locals`, `setattr`, `delattr`, or wildcard imports veto matching throughout the file. Recognizable writes/deletes to an imported module attribute veto that module across aliases. These deliberately broad vetoes can withhold valid matches. Reasons are retained on each call.
29
+
30
+ Assignments of imported objects to new aliases, re-exports, receiver types, dynamic imports, monkeypatching, local modules that shadow installed packages, arbitrary reflective behavior, and cross-file mutation are not resolved. The reviewer cannot authenticate an import's implementation. An unresolved call is neither a defect nor evidence of nonblocking behavior.
31
+
32
+ ## Initial catalogue
33
+
34
+ The intentionally narrow blocking-call catalogue contains:
35
+
36
+ - `time.sleep` ([Python reference](https://docs.python.org/3.12/library/time.html#time.sleep)).
37
+ - `subprocess.run`, `call`, `check_call`, and `check_output` ([Python reference](https://docs.python.org/3.12/library/subprocess.html)).
38
+ - `os.system` ([Python reference](https://docs.python.org/3.12/library/os.html#os.system)).
39
+ - `urllib.request.urlopen` ([Python reference](https://docs.python.org/3.12/library/urllib.request.html#urllib.request.urlopen)).
40
+
41
+ Matches are review candidates, including calls with documented intent or zero-valued arguments. No duration, reachability, valid arguments, defect, or penalty is inferred. Names such as `.read()`, `.wait()`, `.result()`, `.get()`, and `.sleep()` without a supported import binding are not catalogue matches. This is not complete I/O or CPU-blocking detection.
42
+
43
+ Thread-helper syntax is recorded for `asyncio.to_thread`, `anyio.to_thread.run_sync`, and `starlette.concurrency.run_in_threadpool`. `asgiref.sync.sync_to_async` construction and an immediate invocation of the resulting wrapper are separate categories. The latter's two AST calls must not be added together as two offloaded operations. A stored or decorated wrapper is not followed through later calls. `loop.run_in_executor` remains unresolved because its receiver type is unknown. See the [Python thread helper](https://docs.python.org/3.12/library/asyncio-task.html#asyncio.to_thread), [AnyIO thread guide](https://anyio.readthedocs.io/en/stable/threads.html), [Starlette thread pool](https://starlette.dev/threadpool/), and [Django adapter guidance](https://docs.djangoproject.com/en/5.2/topics/async/#sync-to-async).
44
+
45
+ ```python
46
+ await asyncio.to_thread(time.sleep, 1) # Helper call; callback reference is not a sleep call.
47
+ await asyncio.to_thread(time.sleep(1)) # Inner sleep is evaluated before the helper call.
48
+ await asyncio.to_thread(lambda: time.sleep(1)) # Lambda body is a separate synchronous owner.
49
+ await sync_to_async(work)(value) # Wrapper construction plus wrapped invocation syntax.
50
+ ```
51
+
52
+ Callback expressions are described without claiming callable identity or successful offloading. Only a non-starred first positional callback is described; other layouts remain unresolved. Calls nested in argument expressions remain in their actual lexical context. A thread helper does not blanket-exempt its arguments. The immediate parent `Await` is recorded as syntax only; creating an awaitable is not proof it runs.
53
+
54
+ ## Evidence and limits
55
+
56
+ Each observed function contains owned call locations, execution context (`async-body` or `deferred-generator-body`), category, import-match status/reason, immediate-await syntax, and callback expression information where applicable. Summary categories count expressions rather than execution frequency. Deferred calls are separate from the direct async-body totals. Functions with blocking candidates are a review inventory, not a risk rate or scoring denominator. There are no findings, scores, gates, or recommended concurrency changes.
57
+
58
+ All supplied source facts are regenerated and compared before review. Each file is read again against its digest. This detects changed source, new selected files, altered owners/complexity, and selection drift; it does not lock a concurrently changing filesystem. Source-facts file limits apply, plus 100,000 selected owners and 100,000 observed calls. Budget failures discard all rows, including earlier successful files.
59
+
60
+ The [pinned corpus study](../../shared/scorecard-schema/calibration-runs/python-async-usage-2026-10-15.md) documents useful thread-helper observations, unresolved receivers, and the limits of zero catalogue matches. Its regression runner checks exact prior handler/context/transfer/call-return rows and unchanged Method Complexity inputs and scores. Error Handling remains review-only.
61
+
62
+ The separate [async call-context reviewer](ASYNC_CONTEXT.md) adds unresolved reason/shape inventories and bounded local source links, including simple stored wrappers. It leaves this report's import matching, counts, and packaged contract unchanged. Neither report establishes safe blocking behavior or scores Async/Blocking Usage or Error Handling.
@@ -0,0 +1,84 @@
1
+ # Python behavior evidence before scoring
2
+
3
+ Two review producers extend the Python analyzer: handler disposition paths and bounded local async helper paths. These raw reports remain unscored. Python adapter 0.15.0 packages the helper report and the separate [Async/Blocking Usage scoring policy](ASYNC_SCORING.md). Handler dispositions and continuations remain repository-only extensions; Error Handling remains unscored. Their review contracts are unchanged.
4
+
5
+ The later [async scoring policy](ASYNC_SCORING.md) consumes and verifies
6
+ these helper paths without changing this review-only contract. It calculates a
7
+ separate, limited-catalogue owner-rate score, now packaged in 0.15.0. Method
8
+ Complexity is unchanged and Error Handling remains unscored.
9
+
10
+ The subsequent [continuation extension](ERROR_HANDLING_CONTINUATIONS.md) follows bounded post-handler returns/raises through enclosing branches and records retained-default syntax, preserving this first disposition policy unchanged.
11
+
12
+ ## Run
13
+
14
+ Python 3.12, standard library only, from the repository root. The handler reviewer, continuation extension and corpus/accuracy scripts below are repository development tools, not installed commands; the archive includes only the helper/scoring producers and their CLI support:
15
+
16
+ ```powershell
17
+ py -3.12 analyzers/python/error_handling_dispositions.py E:/repos/project --output TestResults/dispositions.json
18
+ py -3.12 analyzers/python/async_helper_paths.py E:/repos/project --output TestResults/helper-paths.json
19
+ py -3.12 analyzers/python/behavior_accuracy.py --output TestResults/behavior-accuracy.json
20
+ ```
21
+
22
+ The source reviewers accept the existing selection options and optional UUID `--run-id`/`--audit-id`. Target code is parsed, never imported or executed. Outputs require new `.json` files and existing parents. Complete reviews exit 0; incomplete reviews or invalid invocations exit 2. Empty source populations are complete, not evidence of excellent behavior.
23
+
24
+ The repeatable corpus command uses the pinned selections and a saved installed-package evidence directory:
25
+
26
+ ```powershell
27
+ py -3.12 analyzers/python/behavior_review_corpus.py --repos E:/repos --baseline TestResults/python-async-adapters-package-2026-10-24/final --output TestResults/behavior-corpus-new
28
+ ```
29
+
30
+ It verifies clean pinned checkouts, exact source facts, unchanged prior exception and async reports (apart from fresh invocation IDs/timestamps), and input immutability. It records implementation hashes, per-project row digests, summaries, and examples. No score is calculated or modified.
31
+
32
+ ## Handler dispositions
33
+
34
+ Policy: `python/exception-disposition-paths-review-v1`.
35
+
36
+ The pass follows statement sequences and both sides of `if`/`elif`/`else`, retaining terminal paths while evaluating subsequent statements only for fallthrough paths. It unions finite outcome states instead of enumerating exponentially many paths. Conditions are not evaluated and infeasible branches are not pruned. All observations are conditional on expression evaluation completing normally; called code can raise, exit, block, or mutate state.
37
+
38
+ | Classification | Meaning |
39
+ | --- | --- |
40
+ | `propagation` | All supported paths reach an explicit raise; bare rethrows and raise expressions remain distinguishable |
41
+ | `explicit-fallback` | Supported paths return explicitly, or match a prior guarded-import/default/adjacent-literal-return shape |
42
+ | `recovery-action` | All supported fallthrough paths contain an explicit call/await or state update; its effectiveness is unknown |
43
+ | `loop-transfer` | Supported paths explicitly break or continue |
44
+ | `mixed-explicit-disposition` | Supported paths combine the preceding explicit actions |
45
+ | `documented-suppression` | An inert handler has qualifying local prose under the existing documentation policy |
46
+ | `unexplained-swallowing-candidate` | An inert handler has no observed local rationale or supported fallback and no identified contextual blocker |
47
+ | `unresolved` | The reviewer cannot establish a supported disposition for the whole suite or its surrounding context |
48
+
49
+ `recovery-action` is an action observation, not resolved recovery. A return does not prove a correct fallback, and a raise does not prove outward propagation. No logging-name whitelist or business-correctness judgment is introduced. Developer prose is trusted as a documented choice without assessing its adequacy; directives alone do not count. Documentation in surrounding code is retained as context and withholds a swallowing candidate rather than automatically approving the handler.
50
+
51
+ Exception groups, enclosing finalizers/context managers, nested handlers, and ambiguous import guards preserve earlier uncertainty. Internal loops, `with`, nested `try`, `match`, declarations, generator suspension, and unsupported expressions remain unresolved. A later `raise` does not erase an earlier unresolved path. Calls inside nested definitions cannot credit the parent.
52
+
53
+ An inert handler also remains unresolved when surrounding structure suggests a probe, selective fallback, loop continuation, or generator protocol: enclosing loops, protected yields, multiple sibling handlers, nearby prose, a try `else`, or following statements outside the recognized literal-return pattern. These are structural guards, not exception-name exemptions. Conditional no-action paths remain unresolved instead of being counted as known swallowing.
54
+
55
+ The pass permits at most 32 nested conditional levels and 100,000 visited statements across the report. A depth limit retains an unresolved path; exhausting the global statement budget fails the report. Each original handler is reported once with its stable identity, source hash, population, and outcome set. Summary totals count handlers, not paths or runtime failures.
56
+
57
+ ## Async helper paths
58
+
59
+ Policy: `python/async-local-helper-paths-review-v1`.
60
+
61
+ Starting at each selected async owner, follow at most three edges into selected, same-file synchronous function bodies. Links use the existing unique, direct, source-order-valid lexical definition rules. Parameter shadowing, rebinding, decorated ambiguity, conditional definitions, receiver methods, cross-file aliases, reflection, and module mutation remain unsupported or veto matching. The catalogue remains the seven supported blocking import spellings; it is not a general I/O detector.
62
+
63
+ Each trace carries its root call and helper call extents, owner IDs, referenced definition where linked, context, and reason. The contexts distinguish:
64
+
65
+ - `async-body`: direct calls and eagerly invoked synchronous helpers.
66
+ - `thread-helper-callback`: callback bodies referenced by supported thread helpers, invoked wrappers, or supported decorators. This is helper syntax, not proof a thread was used or the awaitable ran.
67
+ - `wrapper-callback-reference`: the callback of a wrapper construction; no invocation is inferred.
68
+ - `deferred-generator-body`: deferred generator-expression work; consumption is not inferred.
69
+
70
+ `to_thread(work())` evaluates the helper eagerly, while `to_thread(work)` supplies a callback. A stored/inline partial construction alone does not execute its callback. Invocation through a supported partial is followed; its bound arguments keep their original eager context. Supported stored `sync_to_async` wrappers and decorators retain callback context. Creating a coroutine or synchronous generator does not traverse its body as eager synchronous work.
71
+
72
+ Unresolved paths retain explicit reasons, including the helper depth limit and deferred generator bodies. Local source links do not reduce the original unresolved import-call counts. Declaration expressions, nested function/class/lambda bodies, unknown callback protocols, and transitive async execution remain outside scope. Arguments and conditions are not interpreted; unreachable blocking syntax can remain a candidate.
73
+
74
+ One function with several candidate paths is counted once in `functionsWithDirectOrHelperCandidates`. Repeated paths to the same blocking expression are also summarized as distinct source-call identities. Callback/deferred/reference paths are recorded separately and do not inflate that owner count. Synchronous functions are never added as clean async owners. With no async owners, availability and the candidate-owner count remain unavailable/null.
75
+
76
+ A shared 100,000-step budget bounds measured calls and traversal expansion. Exceeding it discards the extension's rows and summary. Function measurements are cached within each file. Cycles/repeated owners are not expanded; existing source-order restrictions can withhold a recursive link before the cycle guard is reached.
77
+
78
+ ## Integrity and validation boundary
79
+
80
+ Both reviewers regenerate their supplied prior evidence and verify current source facts/selection and source digests. New reports bind the prior row hashes and policy configuration. Drift, incomplete or altered inputs, parser/read errors, and budget failures produce incomplete diagnostic reports with no new rows or summary. Inputs and old classifications remain unchanged. These checks do not authenticate externally edited reports or lock a concurrently changing filesystem.
81
+
82
+ `behavior_accuracy.py` records expected versus actual classifications and candidate-detection confusion counts for 27 curated cases: 15 handler cases and 12 async cases. The suite includes deliberately introduced blocking and silent swallowing, documented choices, coroutine/generator boundaries, and corpus-inspired route, iterator, and selective-fallback counterexamples. These are regression labels, not an independent holdout, a representative accuracy estimate, or a claim of runtime correctness. Additional unit tests cover tampering, budgets, CLI output preservation, nested ownership, source drift, and provenance vetoes.
83
+
84
+ No formula should use recognized-shape rates as handling-success rates or count unresolved observations as defects. Error Handling still requires further candidate review and a separate scoring decision. Async scoring availability, exclusions, and packaging are specified in ASYNC_SCORING.md; this helper report remains review evidence rather than a score.
@@ -0,0 +1,96 @@
1
+ # Experimental Python coverage and CRAP review
2
+
3
+ The reviewer imports a coverage.py JSON report and produces per-function review evidence. Without a collection receipt, estimates remain **conditional**. The separate, explicit collector runs a caller-selected test module and binds its newly generated report to source observations before and after execution. Neither command installs dependencies. The development package now embeds this workflow in `codemetrics-python` (adapter `0.7.0`) and exposes explicit test collection as `codemetrics-python-collect`. The scorecard skill imports the artifacts and reports their limitations. Source-facts v2 and its producer `0.4.0-experimental` remain unchanged. Method Complexity is scored separately; coverage/CRAP does not supply a Testing score or modify complexity. Published npm 0.5.0 does not include these commands; use a newly built local archive.
4
+
5
+ ## Packaged workflow
6
+
7
+ ```powershell
8
+ codemetrics-python --project E:/repos/my-project --include 'src/**' --coverage-json E:/temp/my-project-coverage/coverage.json --collection-manifest E:/temp/my-project-coverage/provenance.json --scorecard-output evidence.json
9
+ codemetrics-evidence --input evidence.json --inspect-output inspection.json
10
+ ```
11
+
12
+ Coverage is optional. Evidence embeds the review at `dimensions.codeQuality.coverageReview`, sharing the exact source population and invocation IDs with complexity and duplication. Missing coverage produces explicit unavailable rows. Incomplete coverage makes the combined run incomplete and withholds all findings and the Method Complexity score. The shared validator checks identities, source membership/digests, line accounting, CRAP arithmetic, receipt consistency, and unavailable/conditional/source-bound summaries. It never executes tests or rereads current source when validating saved evidence; a historical evidence import retains only the binding established at generation time. Package validation is an integrity check, not cryptographic authentication of an unsigned receipt.
13
+
14
+ The helper forwards `--python-coverage-json` and `--python-collection-manifest`; it does not invoke the collector. Coverage/CRAP remains unscored and Testing remains unavailable. Only Method Complexity supports a dimension score and compatible score-drop gates. There is no CSV.
15
+
16
+ The standalone reviewer remains available:
17
+
18
+ ```powershell
19
+ # Run from the CodeMetrics.AI checkout; Python 3.12 is required.
20
+ py -3.12 analyzers/python/coverage_review.py E:/repos/my-project --include 'src/**' --coverage-json E:/repos/my-project/coverage.json --output TestResults/python-coverage-review.json
21
+ ```
22
+
23
+ The target is a directory or Python source file. A file uses its parent as the root. Coverage file keys must be relative to this exact root. Generate reports from that root with your existing coverage workflow; do not move a report from another checkout and assume it still matches. Both `/` and Windows `\` separators are accepted, with case-sensitive exact path matching. Absolute paths and parent traversal are rejected. There is no basename or suffix matching. Extra report files are listed in `summary.unmatchedReportFiles`; selected files absent from the report stay unavailable.
24
+
25
+ The CLI accepts the source analyzer's include/exclude and population flags, plus optional UUID `--run-id` and `--audit-id`. Output must be a new `.json` file with an existing parent. Without `--coverage-json`, it emits explicit unavailable coverage for each function. A specified but missing or malformed report is a failure, not an absent optional measurement.
26
+
27
+ ## Collect new coverage with source provenance
28
+
29
+ Use Python 3.12 with coverage.py and the project's test dependencies already installed in an isolated environment. Validation covers `coverage==7.6.1` and `coverage==7.16.1`. The collector requires a project directory, an explicit new output directory outside that project with an existing parent, and an interpreter path. It executes pytest by default; `--module` selects another Python test module. Arguments after `--` belong to that module.
30
+
31
+ ```powershell
32
+ codemetrics-python-collect E:/repos/my-project --include 'src/**' --python E:/temp/my-project-tests/Scripts/python.exe --output-directory E:/temp/my-project-coverage-001 -- -q tests
33
+ codemetrics-python --project E:/repos/my-project --include 'src/**' --coverage-json E:/temp/my-project-coverage-001/coverage.json --collection-manifest E:/temp/my-project-coverage-001/provenance.json --scorecard-output TestResults/my-project-evidence.json
34
+ ```
35
+
36
+ This is an explicit **test execution** workflow. The source analyzer and offline reviewer still never import or execute target code. Collector policy `python/coverage-collection-v1`:
37
+
38
+ 1. Inventories and hashes selected source before invoking tests.
39
+ 2. Creates new coverage data in a separate process with a unique bytecode cache, avoiding reuse of stale target bytecode. It never combines or binds pre-existing coverage data.
40
+ 3. Runs the test module from the project root and records its command, exit status, Python/coverage versions, and output log. The default timeout is 1,800 seconds; `--timeout` must be positive and finite. A timeout terminates the collector's child process tree and records failure.
41
+ 4. Generates fresh branch-enabled coverage JSON, then repeats the source snapshot. Selected file additions, removals, content changes, or population changes invalidate the collection. Ignored cache-directory creation does not.
42
+ 5. Writes `provenance.json` with a collection UUID, start/end timestamps, source snapshot digests, selection rules/inventory, and the exact JSON report digest. Failed tests, missing tooling, failed reporting, source drift, and timeout leave an incomplete receipt that cannot authenticate review results.
43
+
44
+ The controlled coverage configuration sets `branch=True`, `config_file=False`, and source to the selected project root. Project coverage configuration and inherited `COVERAGE_*`/`PYTHON*` variables are not used. Pytest's automatic third-party plugin loading and inherited `PYTEST_ADDOPTS` are disabled to avoid competing coverage sessions; required plugins can be explicitly passed with `-p`. Project pytest configuration still applies. This first collector measures a **single process**; worker/subprocess coverage aggregation is unsupported. Coverage's version-specific default exclusions and inline pragmas still apply and remain visible in the report.
45
+
46
+ The offline reviewer accepts `--collection-manifest` only with a report. It checks successful completion, runtime/configuration, root, source policy/parser, exact report hash, and current selected source against both recorded snapshots. Any mismatch makes the entire review incomplete with no estimates; it never silently falls back to an unverified import. A valid binding is labeled `matched-pre-post-snapshots`, with freshness `source-matched`. `collectionProvenance.collectionId` identifies the test collection; new review run/audit IDs identify only the later review. No timestamp is refreshed to imply new tests.
47
+
48
+ Bound rows use `coverage.status: reported-source-matched` and `crapEstimate.disposition: review-only`; unbound rows retain `reported-unverified` and `conditional-review-only`. Summary counts separate `sourceBoundEstimates` from `conditionalEstimates`. The embedded review schema is 2, producer `0.3.0-experimental`, which embeds the collection receipt for offline integrity checks. Earlier standalone reports remain historical output. The line measurement policy and arithmetic are unchanged. The source checkout also retains `coverage_collect.py` and `coverage_review.py` for direct Python use.
49
+
50
+ ## What is measured
51
+
52
+ Policy `python/coveragepy-line-crap-review-v1` combines current `python/owned-function-complexity-v2` measurements with reported executable **line** coverage. It reads coverage.py JSON formats 2 and 3 and uses file-level executed, missing, and excluded line sets. Function names and reported summary percentages are not trusted for mapping. Branch counts, contexts, and function/class summary regions are not used in the calculation.
53
+
54
+ The denominator is executed plus missing body lines, after removing coverage exclusions. Definition headers, decorators, parameters, annotations, and a leading docstring do not add coverage credit. Coverage exclusions remain visible and do not suppress complexity decisions. Multiline statements do not get credit for every physical line, and several statements sharing one line remain one coverage observation. This is a limitation of line coverage, not a replacement for statement-based size measurements.
55
+
56
+ Attribution is deliberately narrow:
57
+
58
+ - Ordinary multi-line functions, methods, and async functions can receive estimates.
59
+ - Lambdas and bodies sharing a definition/header line are unavailable. Definition execution is not proof of a call.
60
+ - Bodies sharing their first executable line with a leading docstring are unavailable.
61
+ - Parents containing nested functions, classes, or lambdas are unavailable. Independently selected nested function bodies can still receive estimates.
62
+ - Empty, docstring-only, or entirely excluded bodies without reportable lines are unavailable, not 0% or 100% covered.
63
+ - Missing coverage is unavailable. A body explicitly reported entirely missing is 0% covered.
64
+
65
+ For attributable bodies:
66
+
67
+ ```text
68
+ conditional CRAP estimate = CC² × (1 − coveredLines / executableLines)³ + CC
69
+ ```
70
+
71
+ For CC 10, coverage of 0%, 50%, and 100% produces 110, 22.5, and 10 respectively. Full coverage never removes the underlying complexity. Values are calculated from integer counts without rounding the coverage fraction first. No threshold, grade, project aggregate, score deduction, or test-quality claim is attached.
72
+
73
+ The [original CRAP proposal](https://www.artima.com/weblogs/viewpost.jsp?thread=215899) uses this arithmetic with basis-path coverage. Our executable-line substitution is explicitly labeled `line-coverage-proxy`; it does not establish equivalent calibration. The [coverage.py JSON documentation](https://coverage.readthedocs.io/en/latest/commands/cmd_json.html) describes report generation. Its [format-3 producer](https://github.com/nedbat/coveragepy/blob/7.6.1/coverage/jsonreport.py) records file line observations and region summaries; branch-enabled percentages can combine line and branch information. We calculate a line-only fraction instead of importing that percentage.
74
+
75
+ ## Provenance and failure boundaries
76
+
77
+ **coverage.py JSON does not bind its observations to source hashes.** Imports without a valid collection receipt therefore retain `sourceBinding: unverified`, `freshness: unverified`, and conditional estimates. A parseable timestamp, a matching filename, or a new review run ID cannot establish current coverage. Even unchanged line counts cannot detect a same-size source edit. The importer also cannot prove that automated assertions, rather than manual execution, produced an arbitrary report.
78
+
79
+ The artifact records the exact report's SHA-256, reported timestamp/version/format, fresh review identities, current source hashes, parser and complexity policy, and selection fingerprint. Source rereads must match the current population pass. Source hashes use the existing decoded-text, normalized-newline convention; report hashes cover raw bytes. Optional collection provenance binds these inputs to the recorded test invocation through matching source observations.
80
+
81
+ Receipts are local and unsigned. They protect against accidental stale source/report reuse, not deliberate joint falsification of a report and receipt. Pre/post snapshots cannot detect source edits reverted during execution and are not an immutable filesystem. Binding covers selected source, not unselected tests, dependency/environment changes, or test assertion quality. `source-matched` means the selected source still matches that historical collection; it does not claim a recent test run or current runtime behavior. No coverage or CRAP measurement proves correctness.
82
+
83
+ `status: complete` means the offline import and analysis completed, not that coverage is fresh, complete, or usable for scoring. A parse/read failure, duplicate or conflicting JSON/path/line data, unsupported format, selected-file line beyond the current source, source drift during analysis, or resource-limit breach makes the report incomplete. Incomplete output has diagnostics, no function results, and no summary. Exit codes are 0 for complete and 2 for failure. Resource budgets are 32 MiB of report JSON, 10,000 report files, 100,000 selected functions, and the existing source-selection budgets.
84
+
85
+ ## Verification and next step
86
+
87
+ The [authored fixture](tests/fixtures/coverage/README.md) includes a real coverage.py 7.6.1 format-3 report generated on Python 3.12, including the never-called one-line function trap. Offline tests cover format-2 field compatibility, formula boundaries, absent reports/files, exclusions, source selection, exact paths, malformed input, source drift, fresh IDs, and output preservation. Collection tests add same-size edits, report substitution, source additions/deletions, failed receipts, parser/selection mismatch, cache creation, and output reuse. Four integration tests execute only authored fixtures, covering a successful integer-enum exit, async/threaded body attribution, failures, mutation, and process timeout. CI defines coverage.py 7.6.1 and 7.16.1 on Ubuntu and Windows; without coverage.py, those four tests explicitly skip.
88
+
89
+ ```powershell
90
+ # In a Python 3.12 environment with coverage==7.6.1 installed:
91
+ python -m unittest discover -s analyzers/python/tests -v
92
+ ```
93
+
94
+ The [Requests validation](../../shared/scorecard-schema/calibration-runs/python-coverage-provenance-2026-10-12.md) exercised the collector against a pinned real checkout: 255 tests passed, one skipped, and 238 function estimates were source-bound. This was a three-module test slice, not a full Requests test-coverage assessment. The substituted real report was rejected. Package and skill integration retain these boundaries; broader test-suite/platform validation and any dimension-score calibration remain separate work.
95
+
96
+ The [Starlette validation](../../shared/scorecard-schema/calibration-runs/python-coverage-starlette-2026-10-13.md) exercised asyncio and Trio tests with coverage.py 7.16.1. Seven full-suite Windows failures reproduced without collection and correctly withheld estimates. A separately labeled subset passed 1,338 tests and produced 459 source-bound estimates; it does not represent a passing full-suite run. Both coverage versions passed all 64 Python regression tests locally on Windows.
@@ -0,0 +1,46 @@
1
+ # Experimental Python decomposition evidence
2
+
3
+ This source-only reviewer measures **owned AST statements** and **maximum block nesting** independently. It shares the current source-facts selection and complexity policy, but adds no scores, gates, deductions, or automatic extraction recommendations. It never imports or executes target code.
4
+
5
+ ```powershell
6
+ py -3.12 analyzers/python/decomposition.py E:/repos/my-project --include 'src/**' --output TestResults/decomposition.json
7
+ py -3.12 analyzers/python/decomposition_corpus.py --repos E:/repos --output TestResults/python-decomposition-new
8
+ ```
9
+
10
+ Python 3.12 is required. The standalone CLI accepts directories or Python files and the same include/exclude/population switches as `source_facts.py`. A manifest is not a source-file entry point; pass its containing directory. `--output` requires a new `.json` file with an existing parent. Supplied UUID `--run-id` and `--audit-id` are preserved; otherwise both are freshly generated. Exit codes are 0 for complete, including empty selections, and 2 for incomplete measurements or invalid invocation.
11
+
12
+ The development `codemetrics-python` adapter `0.13.0` embeds this report at `dimensions.codeQuality.decomposition` under ruleset `python-2026-10-16-async-usage-review-v1`. The npm archive includes the producer and this policy document. The scorecard skill requires that local archive and presents separate statement/nesting observations; only Method Complexity is scored; decomposition remains unscored. Existing complexity, duplication, and coverage measurement policies are unchanged.
13
+
14
+ The package analyzes decomposition automatically from the same source-facts population and invocation IDs. Its validator checks producer/selection provenance, unique and complete owner membership, source digests, statement accounting, nesting bounds, and recomputed per-population nearest-rank summaries. These are integrity checks, not an independent AST remeasurement or authentication of an edited report. A missing or invalid report fails validation; an incomplete decomposition review makes the combined run incomplete and withholds its score and findings. Historical `0.5.0-experimental` coverage-era evidence remains valid under its original contract, without invented decomposition measurements. No new decomposition findings, score gates, or thresholds are enabled.
15
+
16
+ ## Policy: `python/owned-statements-block-nesting-v1`
17
+
18
+ Each selected `def`, `async def`, or method owns its body. Count each included `ast.stmt` node once, including control statements and the statements in their suites. A multiline call is one expression statement; semicolon-separated statements remain separate. A chained assignment is one statement. Comments and formatting do not contribute.
19
+
20
+ The leading function docstring, `pass`, bare ellipsis, `global`, `nonlocal`, annotation-only assignments, and Python type-alias declarations are excluded. Imports, assignments with values, calls, returns, raises, assertions, deletion, and other executable statement forms remain counted. This is a source-syntax convention, not a count of runtime instructions: a later bare string expression remains an expression statement.
21
+
22
+ Nested function, lambda, and class definitions form boundaries. Their headers, decorators, defaults, annotations, and bodies do not contribute to the parent's measurement. Selected nested functions and methods are independently measured; class initializer statements are outside this function metric. `excludedSyntax` makes these boundaries visible. Moving work into an unmeasured initializer does not establish better decomposition.
23
+
24
+ Lambdas remain selected owners for complexity, but receive `status: unavailable`, reason `lambda-expression-body`, and `measurement: null` here. Do not treat them as zero-statement implementations. Declaration-only functions excluded by the source-facts policy remain visible in selection provenance. A selected implementation containing only excluded syntax can legitimately have zero owned statements; that is a measurement, not a perfect score.
25
+
26
+ ### Block nesting
27
+
28
+ The function body starts at depth zero. Each `if`, loop, `with`/`async with`, `try`/`try*`, or `match` adds one level for its suites. `else`, `except`, `finally`, and match cases are sibling arms at their containing construct's depth. They do not add another level by themselves. An `elif` is a sibling condition at the same depth as its initial `if`; an explicit nested `if` inside `else` adds depth. Source indentation positions distinguish the two because their unpositioned ASTs can otherwise be identical.
29
+
30
+ Comprehensions, boolean expressions, conditional expressions, and patterns do not add **block** nesting or standalone statements. The existing complexity metric still counts its applicable decisions. This policy does not measure expression size, expression nesting, runtime call depth, or cognitive complexity. Context managers and cleanup scopes can produce justified nesting; the measurement does not recommend removing them.
31
+
32
+ ### Evidence and population
33
+
34
+ Each function row records its stable source-facts ID, location, source digest, population, synchronous/async/lambda kind, and existing CC. A measured row adds `ownedStatements`, `maxBlockNesting`, a statement-kind histogram, and counts of excluded syntax. All rows are `review-only`.
35
+
36
+ Summaries partition production, test, support, and generated populations and distinguish selected, measured, and unavailable bodies. Each measured population reports nearest-rank p50/p90/p95/max independently for size and nesting: quantile index is `ceil(percent * count / 100) - 1`. An empty measured population has null distributions, not zero percentiles. Do not merge populations or projects into a score.
37
+
38
+ The report includes source-selection provenance, source-facts producer/policy, a versioned measurement policy, effective configuration/fingerprint, and fresh review IDs. Source files are reread with the existing hash, link, and file-size checks. Owner identity must match the original population. Selected-source errors, changed content/owners, parser failures, or the 100,000-owner budget make the entire review incomplete, with diagnostics but no function rows or summary. No partial result can imply a clean project. These observations are not an immutable-filesystem guarantee or authentication of an edited report.
39
+
40
+ ## Corpus review and calibration
41
+
42
+ The [first corpus study](../../shared/scorecard-schema/calibration-runs/python-decomposition-2026-10-13.md) covers the pinned Requests, Starlette, HTTPie, and Django selections. It verifies unchanged source digests and every existing function fact, including CC; it saves full rows and separate size/nesting rankings. Large low-CC bodies use the existing CC 1–5 band for sampling, not an exemption or a size threshold.
43
+
44
+ The initial study supports separate review signals. Long registration and initialization routines show why statement count alone is insufficient for an automatic decomposition penalty. Short but deeply nested routines show why size alone can miss meaningful structure. Neither extreme proves a defect or safe refactoring opportunity. Threshold counts in the study are calibration observations only; this implementation contains no decomposition scoring ladder.
45
+
46
+ Automated checks run with normal Python test discovery. They protect wrapping/semicolon semantics, ownership, declaration and lambda boundaries, flat versus nested conditions, exception and match arms, async constructs, expression scope, separate populations, empty distributions, provenance failure, non-execution, IDs, and output preservation.
@@ -0,0 +1,36 @@
1
+ # Experimental Python duplication review
2
+
3
+ This capability reports exact repeated function bodies for source review. The development `codemetrics-python` package (adapter `0.7.0`) embeds it at `dimensions.codeQuality.duplication` in evidence v3. The code-scorecard skill presents it as unscored review evidence. The standalone CLI remains available for focused analysis. Coverage integration does not change the duplication policy or complexity formula.
4
+
5
+ ```powershell
6
+ py -3.12 analyzers/python/duplication.py E:/repos/my-project --include 'src/**' --output TestResults/duplication.json
7
+ py -3.12 analyzers/python/duplication_corpus.py --repos E:/repos --output TestResults/python-duplication-new
8
+ ```
9
+
10
+ Python 3.12 is required. Target source is parsed without imports, dependency installation, or execution. The first command accepts the same file selection flags as `source_facts.py`; its target is a directory or Python file, not a project manifest. Output must be a new `.json` file with an existing parent directory. The corpus runner requires clean pinned checkouts and a new output directory.
11
+
12
+ ## Measurement boundary
13
+
14
+ Policy `python/exact-function-body-v1` compares whole selected implementation bodies after native AST parsing. It omits source positions and a leading function docstring. Comments and formatting do not enter the AST. It retains identifier names, attributes, literal values, operators, statement order, and all other body structure. Equality uses the complete normalized structure; the displayed hash is an identity, not the equality test.
15
+
16
+ Function names, signatures, default parameters, decorators, and enclosing classes are outside the compared body. The same body may refer to different bindings or behave differently in different contexts. Equal bodies do **not** prove semantic equivalence, interchangeable APIs, unnecessary code, or safe extraction. Conversely, renamed copies, partial blocks, and structurally different implementations are not detected. Literal spellings that parse to the same value can match; this is structural equality, not byte equality.
17
+
18
+ The default minimum is **five statement nodes** per body. Statements within control-flow blocks count; physical lines do not determine eligibility. The leading docstring, `pass`, `global`, `nonlocal`, and annotations without values do not contribute to this count, although non-docstring syntax remains part of the equality key. `--minimum-statements` changes the minimum (integer ≥ 2) and the configuration fingerprint. This is an initial noise-control threshold, not an established refactoring threshold.
19
+
20
+ Parents containing a nested function, class, or lambda are excluded rather than compared with missing body regions. Nested function implementations are still measured separately if independently eligible; lambdas are excluded from this whole-statement-body proof. Async and synchronous functions are compared separately. Production, test, support, and generated populations are also kept separate, even when selection overrides include them. Recognized declaration-only bodies and excluded files follow the existing source-selection policy.
21
+
22
+ Each group has a stable policy-specific ID, body digest, statement count, and sorted occurrences with function ID, relative file, function start/end lines, population, and source digest. A body occurring three times forms one group with three occurrences, not three pairwise findings. No overlapping block counts or duplicated-line percentage is produced. Summary counts distinguish selected functions, eligible functions, excluded functions, matching groups, and matched functions; zero matches only describes this narrow search.
23
+
24
+ ## Freshness and incomplete analysis
25
+
26
+ Each invocation gets a fresh run ID and audit ID; UUID-valued `--run-id` and `--audit-id` allow a caller to supply its own. IDs are separate from body fingerprints. The report records selection inventory, parser/producer versions, policy, effective configuration, and configuration fingerprint. Source digests must match the initial population pass when files are reread. This checks consistency between reads; it is not a filesystem snapshot or a promise that source cannot change after observation.
27
+
28
+ A selected parse/read failure, source mismatch, changed function identity, or exceeded resource budget makes the report incomplete. Incomplete reports expose diagnostics and inventory, with **no groups and no summary**. No partial results become a score or a claim of no duplication. Complete empty selections explicitly report zero eligible functions. Exit codes are 0 for complete and 2 for incomplete or CLI errors. Prior output files are never overwritten or reused.
29
+
30
+ In addition to the source analyzer's file-size and file-count limits, this proof allows at most 100,000 selected owners and 64 MiB of normalized eligible bodies. Limits are recorded in the configuration. It fails explicitly rather than truncating a group list. The standalone report remains its own experimental artifact. The packaged analyzer embeds it in the extensible Code Quality section of evidence v3, alongside unchanged source-facts v2. Packaged runs fix the minimum at five statements, share the exact source population and invocation IDs, and validate source provenance, owner membership, exclusions, and group accounting. Validation checks evidence consistency, not equivalence by rereading target source. If either measurement is incomplete, the combined analysis is incomplete and its Method Complexity score and findings are withheld.
31
+
32
+ The [first corpus review](../../shared/scorecard-schema/calibration-runs/python-duplication-2026-10-10.md) records findings and limitations. Automated tests run through the existing Python CI discovery command:
33
+
34
+ ```powershell
35
+ py -3.12 -m unittest discover -s analyzers/python/tests -v
36
+ ```