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,181 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ import { fingerprint } from "./python-error-context.js";
4
+ export const asyncContextPolicy = "python/async-call-context-review-v1";
5
+ const referenceReasons = ["non-name-reference", "comprehension-local-binding", "reflective-or-cross-scope-binding",
6
+ "rebound-or-ambiguous-name", "not-a-direct-function-definition", "decorated-definition", "definition-not-before-reference",
7
+ "definition-outside-selected-owners", "no-local-definition"];
8
+ const wrapperReasons = ["comprehension-local-binding", "reflective-or-cross-scope-binding", "deferred-execution-context",
9
+ "not-a-unique-same-owner-binding", "not-a-direct-wrapper-assignment", "assignment-not-before-use", "unmatched-wrapper-constructor", "unresolved-argument-layout"];
10
+ const shapes = ["name", "attribute-path", "attribute-on-expression", "call-result", "subscript", "lambda", "other-expression"];
11
+ const contexts = ["async-body", "deferred-generator-body"];
12
+ function requireValue(condition, message) {
13
+ if (!condition)
14
+ throw new Error(`Invalid Python async context evidence: ${message}.`);
15
+ }
16
+ export function keys(value, names) {
17
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
18
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), [...names].sort()), "unexpected or missing fields");
19
+ }
20
+ const integer = (n) => Number.isSafeInteger(n) && n >= 0;
21
+ const nonempty = (s) => typeof s === "string" && s.trim().length > 0;
22
+ export const before = (a, b) => a.endLine < b.line || a.endLine === b.line && a.endColumn < b.column;
23
+ export const contains = (a, b) => (a.line < b.line || a.line === b.line && a.column <= b.column) &&
24
+ (a.endLine > b.endLine || a.endLine === b.endLine && a.endColumn >= b.endColumn);
25
+ export function extent(value) {
26
+ requireValue([value.line, value.column, value.endLine, value.endColumn].every(integer) && value.line > 0 &&
27
+ (value.endLine > value.line || value.endLine === value.line && value.endColumn > value.column), "invalid extent");
28
+ }
29
+ export function reference(value, spelling, target, kind, use, owner, owners, bindingReason) {
30
+ keys(value, value.status === "import-spelling" ? ["status", "reason", "target", "definition"] : ["status", "reason", "definition"]);
31
+ if (target !== null) {
32
+ requireValue(value.status === "import-spelling" && value.target === target && value.reason === "import-spelling-only" && value.definition === null, "callback import mismatch");
33
+ return;
34
+ }
35
+ if (kind === "Name" && ["comprehension-local-binding", "reflective-or-cross-scope-binding"].includes(bindingReason))
36
+ requireValue(value.status === "unresolved" && value.reason === bindingReason, "source binding veto must be preserved");
37
+ if (value.status === "unresolved") {
38
+ requireValue(value.definition === null && referenceReasons.includes(value.reason), "invalid unresolved reference");
39
+ requireValue(kind === "Name" || value.reason === "non-name-reference", "non-name reference cannot resolve locally");
40
+ return;
41
+ }
42
+ requireValue(value.status === "local-definition" && value.reason === "unique-direct-lexical-definition" && kind === "Name", "invalid local reference");
43
+ keys(value.definition, ["ownerId", "file", "member", "functionKind", "line", "column", "endLine", "endColumn"]);
44
+ const d = value.definition, selected = owners.get(d.ownerId);
45
+ extent(d);
46
+ requireValue(selected && selected.file === owner.file && d.file === selected.file && d.member === selected.member &&
47
+ d.line === selected.line && d.endLine === selected.endLine && d.functionKind === selected.functionKind &&
48
+ ["FunctionDef", "AsyncFunctionDef"].includes(d.functionKind) && spelling === d.member.split(".").at(-1), "unknown local definition");
49
+ requireValue(d.ownerId === hash(`${d.file}|${d.line}|${d.column}|${d.functionKind}`) && before(d, use), "definition identity or source order mismatch");
50
+ }
51
+ function wrapper(value, call, owner, calls) {
52
+ keys(value, ["status", "reason", "definition"]);
53
+ if (["comprehension-local-binding", "reflective-or-cross-scope-binding"].includes(call.binding.reason))
54
+ requireValue(value.status === "unresolved" && value.reason === call.binding.reason, "wrapper binding veto must be preserved");
55
+ if (value.status === "unresolved") {
56
+ requireValue(value.definition === null && wrapperReasons.includes(value.reason), "invalid unresolved wrapper");
57
+ return null;
58
+ }
59
+ requireValue(value.status === "stored-wrapper-syntax" && value.reason === "unique-direct-same-owner-assignment" && call.executionContext === "async-body", "invalid stored wrapper");
60
+ keys(value.definition, ["name", "assignment", "constructionCallId", "construction", "importTarget"]);
61
+ const d = value.definition;
62
+ keys(d.assignment, ["line", "column", "endLine", "endColumn"]);
63
+ extent(d.assignment);
64
+ keys(d.construction, ["line", "column", "endLine", "endColumn"]);
65
+ extent(d.construction);
66
+ const construction = calls.get(d.constructionCallId);
67
+ requireValue(d.name === call.binding.syntax && d.importTarget === "asgiref.sync.sync_to_async" &&
68
+ d.assignment.line >= owner.line && d.assignment.endLine <= owner.endLine && contains(d.assignment, d.construction) && before(d.assignment, call), "wrapper extent/name mismatch");
69
+ requireValue(construction && construction.category === "offload-wrapper-construction" && construction.executionContext === "async-body" &&
70
+ construction.callback && construction.callback.kind !== "unresolved-argument-layout" && !construction.directlyAwaited &&
71
+ d.constructionCallId === fingerprint([owner.id, d.construction]) &&
72
+ ["line", "column", "endLine", "endColumn"].every(key => construction[key] === d.construction[key]), "missing wrapper construction");
73
+ return construction;
74
+ }
75
+ function callContext(value, call, owner, owners, calls, rows) {
76
+ keys(value, ["callId", "executionContext", "shape", "unresolvedReason", "localCallee", "storedWrapper", "callback"]);
77
+ requireValue(value.callId === call.id && value.executionContext === call.executionContext && shapes.includes(value.shape), "call identity/context/shape mismatch");
78
+ const spelling = call.binding.syntax;
79
+ if (call.category === "offload-wrapped-invocation")
80
+ requireValue(value.shape === "call-result", "immediate wrapper shape mismatch");
81
+ else if (spelling !== null)
82
+ requireValue(value.shape === (spelling.includes(".") ? "attribute-path" : "name"), "callee shape differs from spelling");
83
+ else
84
+ requireValue(!["name", "attribute-path"].includes(value.shape), "dynamic callee cannot be a path");
85
+ requireValue(value.unresolvedReason === (call.binding.status === "unresolved" ? call.binding.reason : null), "unresolved reason mismatch");
86
+ let construction = null;
87
+ if (call.binding.status === "unresolved" && value.shape === "name") {
88
+ requireValue(value.localCallee !== null && value.storedWrapper !== null, "name context required");
89
+ reference(value.localCallee, spelling, null, "Name", call, owner, owners, call.binding.reason);
90
+ construction = wrapper(value.storedWrapper, call, owner, calls);
91
+ if (construction)
92
+ requireValue(value.localCallee.status === "unresolved" && value.localCallee.reason === "not-a-direct-function-definition", "wrapper is not a function definition");
93
+ }
94
+ else
95
+ requireValue(value.localCallee === null && value.storedWrapper === null, "non-name cannot claim local links");
96
+ const callback = construction?.callback ?? call.callback;
97
+ if (!callback || callback.kind === "unresolved-argument-layout") {
98
+ requireValue(value.callback === null, "unexpected callback context");
99
+ return;
100
+ }
101
+ keys(value.callback, ["kind", "evaluation", "reference"]);
102
+ requireValue(value.callback.kind === callback.kind && value.callback.evaluation === callback.evaluation, "callback expression mismatch");
103
+ reference(value.callback.reference, callback.binding.syntax, callback.binding.target, callback.kind, construction ?? call, owner, owners, callback.binding.reason);
104
+ if (construction)
105
+ requireValue(isDeepStrictEqual(value.callback, rows.get(construction.id)?.callback), "stored callback differs from construction");
106
+ if (call.category === "offload-wrapped-invocation") {
107
+ const inner = [...calls.values()].find(candidate => candidate.category === "offload-wrapper-construction" && candidate.line === call.line &&
108
+ candidate.column === call.column && contains(call, candidate) && candidate.id !== call.id);
109
+ requireValue(inner && isDeepStrictEqual(value.callback, rows.get(inner.id)?.callback), "immediate callback differs from construction");
110
+ }
111
+ }
112
+ const histogram = (values) => {
113
+ const counts = new Map();
114
+ for (const value of values)
115
+ counts.set(value, (counts.get(value) ?? 0) + 1);
116
+ return Object.fromEntries([...counts.entries()].sort(([a], [b]) => a.localeCompare(b, "en")));
117
+ };
118
+ function inventory(rows, context) {
119
+ const groups = new Map();
120
+ for (const row of rows)
121
+ for (const call of row.calls)
122
+ if (call.executionContext === context && call.unresolvedReason !== null) {
123
+ const key = `${call.unresolvedReason}\0${call.shape}`;
124
+ const group = groups.get(key) ?? { reason: call.unresolvedReason, shape: call.shape, callExpressions: 0, examples: [] };
125
+ group.callExpressions++;
126
+ if (group.examples.length < 3)
127
+ group.examples.push({ ownerId: row.id, callId: call.callId });
128
+ groups.set(key, group);
129
+ }
130
+ return [...groups.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, group]) => group);
131
+ }
132
+ function counts(rows) {
133
+ const eligible = rows.filter(row => row.status === "observed"), calls = eligible.flatMap(row => row.calls);
134
+ return { selectedFunctions: rows.length, asyncFunctions: eligible.length, excludedSynchronousFunctions: rows.length - eligible.length,
135
+ availability: eligible.length ? "available" : "unavailable", contexts: Object.fromEntries(contexts.map(context => {
136
+ const selected = calls.filter(call => call.executionContext === context), unmatched = selected.filter(call => call.unresolvedReason !== null);
137
+ return [context, eligible.length ? { callExpressions: selected.length, unresolvedImportCallExpressions: unmatched.length,
138
+ unresolvedReasons: histogram(unmatched.map(call => call.unresolvedReason)), unresolvedShapes: histogram(unmatched.map(call => call.shape)),
139
+ unresolvedInventory: inventory(eligible, context), localCalleeLinks: selected.filter(call => call.localCallee?.status === "local-definition").length,
140
+ storedWrapperInvocationLinks: selected.filter(call => call.storedWrapper?.status === "stored-wrapper-syntax").length,
141
+ localCallbackReferenceLinks: selected.filter(call => call.callback?.reference.status === "local-definition").length } : null];
142
+ })) };
143
+ }
144
+ /** Check consistency against validated async evidence; not independent AST resolution or authentication. */
145
+ export function validateAsyncContext(value, base) {
146
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "subject", "selection", "sourceFactsProducer", "sourceFactsPolicy", "asyncRowsSha256", "status", "diagnostics", "functions", "summary", "scope"]);
147
+ const r = value;
148
+ requireValue(r.schemaVersion === 1 && r.artifactKind === "experimental-python-async-call-context-review" && r.policy === asyncContextPolicy &&
149
+ isDeepStrictEqual(r.producer, { name: "codemetrics-ai/python-async-call-context", version: "0.1.0-experimental" }), "unsupported producer/policy");
150
+ for (const key of ["runId", "auditId", "subject", "selection", "sourceFactsProducer", "sourceFactsPolicy"])
151
+ requireValue(isDeepStrictEqual(r[key], base[key]), "invocation or source provenance mismatch");
152
+ const configuration = { policy: asyncContextPolicy, asyncConfigurationFingerprint: base.configurationFingerprint, maxExamplesPerGroup: 3 };
153
+ requireValue(isDeepStrictEqual(r.configuration, configuration) && r.configurationFingerprint === fingerprint(configuration) &&
154
+ r.asyncRowsSha256 === fingerprint(base.functions), "configuration or original async rows mismatch");
155
+ requireValue(nonempty(r.scope) && typeof r.generatedAtUtc === "string" && Number.isFinite(Date.parse(r.generatedAtUtc)), "scope/time required");
156
+ requireValue(Array.isArray(r.functions) && Array.isArray(r.diagnostics), "collections required");
157
+ for (const d of r.diagnostics) {
158
+ keys(d, ["code", "message"]);
159
+ requireValue(d.code === "async-context-analysis" && nonempty(d.message), "invalid diagnostic");
160
+ }
161
+ if (r.status === "incomplete") {
162
+ requireValue(r.diagnostics.length > 0 && r.functions.length === 0 && r.summary === null, "incomplete report must withhold rows");
163
+ return;
164
+ }
165
+ requireValue(r.status === "complete" && base.status === "complete" && !r.diagnostics.length && r.functions.length === base.functions.length, "status/owner population mismatch");
166
+ const owners = new Map(base.functions.map(row => [row.id, row]));
167
+ for (let index = 0; index < r.functions.length; index++) {
168
+ const row = r.functions[index], owner = base.functions[index];
169
+ keys(row, ["id", "file", "member", "line", "endLine", "population", "sourceSha256", "status", "reason", "disposition", "calls"]);
170
+ for (const key of ["id", "file", "member", "line", "endLine", "population", "sourceSha256", "status", "reason", "disposition"])
171
+ requireValue(row[key] === owner[key], "owner metadata/order mismatch");
172
+ const original = owner.measurement?.calls ?? [];
173
+ requireValue(Array.isArray(row.calls) && row.calls.length === original.length, "owned call population mismatch");
174
+ const calls = new Map(original.map(call => [call.id, call])), rows = new Map(row.calls.map(call => [call.callId, call]));
175
+ for (let i = 0; i < row.calls.length; i++)
176
+ callContext(row.calls[i], original[i], owner, owners, calls, rows);
177
+ }
178
+ const expected = { ...counts(r.functions), populations: Object.fromEntries([...new Set(r.functions.map(row => row.population))].sort()
179
+ .map(population => [population, counts(r.functions.filter(row => row.population === population))])) };
180
+ requireValue(isDeepStrictEqual(r.summary, expected), "summary or inventory mismatch");
181
+ }
@@ -0,0 +1,89 @@
1
+ import { type Extent } from "./python-async-context.js";
2
+ import type { PythonAsyncContext } from "./python-async-context.js";
3
+ import type { PythonAsyncAdapters } from "./python-async-adapters.js";
4
+ import type { PythonAsyncUsage } from "./python-async-usage.js";
5
+ import { type Dimension } from "./evidence.js";
6
+ export declare const asyncHelperPolicy = "python/async-local-helper-paths-review-v1";
7
+ export declare const pythonAsyncPolicy = "python/async-blocking-owner-rate-v1";
8
+ export declare const asyncIntentPolicy = "adjacent-simple-statement-declared-blocking-prose-v1";
9
+ export declare const asyncLadder: {
10
+ percent: number;
11
+ inclusive: boolean;
12
+ score: number;
13
+ }[];
14
+ interface Step extends Extent {
15
+ ownerId: string;
16
+ callId: string;
17
+ file: string;
18
+ member: string;
19
+ }
20
+ interface Trace {
21
+ path: Step[];
22
+ executionContext: string;
23
+ status: string;
24
+ reason: string;
25
+ target: string | null;
26
+ targetOwnerId?: string;
27
+ }
28
+ interface HelperOwner {
29
+ id: string;
30
+ file: string;
31
+ member: string;
32
+ line: number;
33
+ population: string;
34
+ sourceSha256: string;
35
+ traces: Trace[];
36
+ disposition: string;
37
+ }
38
+ export interface PythonAsyncHelpers extends Omit<PythonAsyncUsage, "functions"> {
39
+ asyncRowsSha256: string;
40
+ functions: HelperOwner[];
41
+ }
42
+ interface Documentation {
43
+ file: string;
44
+ callId: string;
45
+ line: number;
46
+ text: string;
47
+ }
48
+ interface Observation extends Trace {
49
+ documentation: Documentation[];
50
+ classification: string;
51
+ }
52
+ interface Contribution extends Omit<HelperOwner, "traces" | "disposition"> {
53
+ eligible: boolean;
54
+ eligibilityReason: string;
55
+ affected: boolean;
56
+ observations: Observation[];
57
+ unresolvedPaths: Trace[];
58
+ }
59
+ export interface PythonAsyncScoring extends Omit<PythonAsyncHelpers, "asyncRowsSha256" | "functions"> {
60
+ helperRowsSha256: string;
61
+ functions: Contribution[];
62
+ }
63
+ /** Validate provenance, path/source identities and accounting; this is not independent AST replay. */
64
+ export declare function validateAsyncHelpers(value: unknown, base: PythonAsyncUsage, context: PythonAsyncContext, adapters: PythonAsyncAdapters): asserts value is PythonAsyncHelpers;
65
+ export declare function asyncOwnerScore(affected: number, eligible: number): number | null;
66
+ export declare function asyncScoreSummary(rows: Contribution[]): {
67
+ availability: string;
68
+ eligibleOwners: number;
69
+ affectedOwners: number;
70
+ score: number | null;
71
+ affectedRate: {
72
+ numerator: number;
73
+ denominator: number;
74
+ } | null;
75
+ excludedOwnersByPopulation: {
76
+ [k: string]: number;
77
+ };
78
+ observationPathsByClassification: {
79
+ [k: string]: number;
80
+ };
81
+ distinctScoredSourceCalls: number;
82
+ ownersWithUnresolvedPaths: number;
83
+ unresolvedPathsByReason: {
84
+ [k: string]: number;
85
+ };
86
+ };
87
+ export declare function validateAsyncScoring(value: unknown, base: PythonAsyncHelpers, usage: PythonAsyncUsage): asserts value is PythonAsyncScoring;
88
+ export declare function scorePythonAsync(dimension: Dimension, report: PythonAsyncScoring | null, complete: boolean): Dimension;
89
+ export {};
@@ -0,0 +1,249 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { fingerprint } from "./python-error-context.js";
3
+ import { keys, extent } from "./python-async-context.js";
4
+ import { scored, hash } from "./evidence.js";
5
+ import { firstMatch } from "./scoring-decision.js";
6
+ export const asyncHelperPolicy = "python/async-local-helper-paths-review-v1";
7
+ export const pythonAsyncPolicy = "python/async-blocking-owner-rate-v1";
8
+ export const asyncIntentPolicy = "adjacent-simple-statement-declared-blocking-prose-v1";
9
+ const locationKeys = ["line", "column", "endLine", "endColumn"];
10
+ const identityKeys = ["id", "file", "member", "line", "population", "sourceSha256"];
11
+ const contexts = ["async-body", "thread-helper-callback", "wrapper-callback-reference", "deferred-generator-body"];
12
+ const blocking = ["time.sleep", "subprocess.run", "subprocess.call", "subprocess.check_call", "subprocess.check_output", "os.system", "urllib.request.urlopen"];
13
+ export const asyncLadder = [
14
+ { percent: 1, inclusive: false, score: 10 }, { percent: 2, inclusive: false, score: 9 },
15
+ { percent: 5, inclusive: false, score: 8 }, { percent: 10, inclusive: true, score: 6 },
16
+ { percent: 15, inclusive: true, score: 4 }, { percent: 20, inclusive: true, score: 2 }
17
+ ];
18
+ function check(condition, message) {
19
+ if (!condition)
20
+ throw new Error(`Invalid Python async scoring evidence: ${message}.`);
21
+ }
22
+ const histogram = (items) => {
23
+ const counts = {};
24
+ for (const item of items)
25
+ counts[item] = (counts[item] ?? 0) + 1;
26
+ return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
27
+ };
28
+ const nonempty = (value) => typeof value === "string" && value.trim().length > 0;
29
+ const same = (a, b, message) => check(isDeepStrictEqual(a, b), message);
30
+ const location = (step) => Object.fromEntries(locationKeys.map(key => [key, step[key]]));
31
+ const commonKeys = ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "subject", "selection", "sourceFactsProducer", "sourceFactsPolicy", "status", "diagnostics", "scope", "functions", "summary"];
32
+ function envelope(report, base, policy, kind, producer, configuration) {
33
+ check(report.schemaVersion === 1 && report.artifactKind === kind && report.policy === policy, "unsupported policy/kind");
34
+ same(report.producer, { name: producer, version: "0.1.0-experimental" }, "producer mismatch");
35
+ for (const key of ["runId", "auditId", "subject", "selection", "sourceFactsProducer", "sourceFactsPolicy"])
36
+ same(report[key], base[key], "invocation/source mismatch");
37
+ same(report.configuration, configuration, "configuration mismatch");
38
+ check(report.configurationFingerprint === fingerprint(configuration), "configuration fingerprint mismatch");
39
+ check(nonempty(report.scope) && typeof report.generatedAtUtc === "string" && Number.isFinite(Date.parse(report.generatedAtUtc)), "scope/time required");
40
+ check(Array.isArray(report.functions) && Array.isArray(report.diagnostics), "collections required");
41
+ for (const diagnostic of report.diagnostics) {
42
+ keys(diagnostic, ["code", "message"]);
43
+ check(nonempty(diagnostic.code) && typeof diagnostic.message === "string", "invalid diagnostic");
44
+ }
45
+ if (report.status === "incomplete") {
46
+ check(report.diagnostics.length > 0 && !report.functions.length && report.summary === null, "incomplete report must withhold rows/score");
47
+ return false;
48
+ }
49
+ check(report.status === "complete" && base.status === "complete" && !report.diagnostics.length, "completion mismatch");
50
+ return true;
51
+ }
52
+ function helperCounts(rows) {
53
+ const traces = rows.flatMap(row => row.traces), candidates = traces.filter(row => row.status === "blocking-candidate");
54
+ return { asyncFunctions: rows.length, availability: rows.length ? "available" : "unavailable",
55
+ functionsWithDirectOrHelperCandidates: rows.length ? rows.filter(row => row.traces.some(trace => trace.status === "blocking-candidate" && trace.executionContext === "async-body")).length : null,
56
+ candidatePathsByContext: histogram(candidates.map(trace => trace.executionContext)),
57
+ unresolvedPathsByReason: histogram(traces.filter(trace => trace.status === "unresolved").map(trace => trace.reason)),
58
+ candidatePaths: candidates.length, localHelperLinks: traces.filter(trace => trace.status === "local-helper-link").length,
59
+ distinctBlockingSourceCalls: new Set(candidates.map(trace => trace.path.at(-1).callId)).size };
60
+ }
61
+ function validatePath(trace, row, owners, links) {
62
+ keys(trace, ["path", "executionContext", "status", "reason", "target", ...(trace.status === "local-helper-link" ? ["targetOwnerId"] : [])]);
63
+ check(contexts.includes(trace.executionContext) && nonempty(trace.reason), "invalid execution context/reason");
64
+ check(Array.isArray(trace.path) && trace.path.length > 0 && trace.path.length <= 4, "helper depth exceeded");
65
+ const used = new Set();
66
+ for (const [index, step] of trace.path.entries()) {
67
+ keys(step, ["ownerId", "callId", "file", "member", ...locationKeys]);
68
+ extent(step);
69
+ const owner = owners.get(step.ownerId);
70
+ check(owner && owner.file === row.file && step.file === owner.file && step.member === owner.member &&
71
+ step.line >= owner.line && step.endLine <= owner.endLine && !used.has(owner.id), "path owner/location mismatch");
72
+ used.add(owner.id);
73
+ check(step.callId === fingerprint([step.ownerId, location(step)]), "path call identity mismatch");
74
+ if (!index)
75
+ check(step.ownerId === row.id, "path must start at async owner");
76
+ else {
77
+ const prefix = trace.path.slice(0, index);
78
+ const link = links.get(fingerprint(prefix));
79
+ check(link?.targetOwnerId === step.ownerId && (trace.executionContext === link.executionContext || trace.executionContext === "deferred-generator-body" ||
80
+ link.executionContext === "async-body" && ["thread-helper-callback", "wrapper-callback-reference"].includes(trace.executionContext)), "missing or inconsistent helper path prefix");
81
+ check(owner.functionKind === "FunctionDef", "helper must be synchronous");
82
+ }
83
+ }
84
+ if (trace.status === "blocking-candidate")
85
+ check(blocking.includes(trace.target) && ["catalogue-import-spelling", "catalogue-callback-import-spelling"].includes(trace.reason), "invalid blocking catalogue observation");
86
+ else if (trace.status === "local-helper-link") {
87
+ const target = owners.get(trace.targetOwnerId);
88
+ check(trace.target === null && trace.reason === "bounded-lexical-definition" && target?.functionKind === "FunctionDef" &&
89
+ target.file === row.file && !used.has(target.id) && target.line < trace.path.at(-1).line && trace.path.length <= 3, "invalid local helper link");
90
+ }
91
+ else
92
+ check(trace.status === "unresolved" && trace.target === null, "invalid unresolved trace");
93
+ }
94
+ /** Validate provenance, path/source identities and accounting; this is not independent AST replay. */
95
+ export function validateAsyncHelpers(value, base, context, adapters) {
96
+ keys(value, [...commonKeys, "asyncRowsSha256"]);
97
+ const report = value;
98
+ check(report.asyncRowsSha256 === fingerprint(base.functions), "async rows hash mismatch");
99
+ if (!envelope(report, base, asyncHelperPolicy, "experimental-python-async-helper-paths", "codemetrics-ai/python-async-helper-paths", { policy: asyncHelperPolicy, asyncConfigurationFingerprint: base.configurationFingerprint, maxHelperDepth: 3, maxSteps: 100000 }))
100
+ return;
101
+ const observed = base.functions.filter(owner => owner.status === "observed");
102
+ same(report.functions.map(row => row.id), observed.map(row => row.id), "async owner population/order mismatch");
103
+ const owners = new Map(base.functions.map(row => [row.id, row]));
104
+ let total = 0;
105
+ for (const row of report.functions) {
106
+ keys(row, [...identityKeys, "traces", "disposition"]);
107
+ const original = owners.get(row.id);
108
+ for (const key of identityKeys)
109
+ same(row[key], original[key], "owner provenance mismatch");
110
+ check(row.disposition === "review-only" && Array.isArray(row.traces), "invalid helper row");
111
+ total += row.traces.length;
112
+ check(total <= 200000, "trace budget exceeded");
113
+ const links = new Map(row.traces.filter(trace => trace.status === "local-helper-link").map(trace => [fingerprint(trace.path), trace]));
114
+ const unique = new Set();
115
+ const calls = new Map(original.measurement.calls.map(call => [call.id, call]));
116
+ const contextual = new Map(context.functions.find(owner => owner.id === row.id).calls.map(call => [call.callId, call]));
117
+ const adapted = new Map(adapters.functions.find(owner => owner.id === row.id).calls.map(call => [call.callId, call]));
118
+ for (const trace of row.traces) {
119
+ validatePath(trace, row, owners, links);
120
+ const identity = fingerprint(trace);
121
+ check(!unique.has(identity), "duplicate trace");
122
+ unique.add(identity);
123
+ const root = trace.path[0], call = calls.get(root.callId);
124
+ check(call && isDeepStrictEqual(location(root), location(call)), "unknown root call");
125
+ if (trace.path.length !== 1)
126
+ continue;
127
+ if (call.category === "blocking-call-candidate") {
128
+ check(trace.status === "blocking-candidate" && trace.target === call.binding.target && trace.executionContext === call.executionContext && trace.reason === "catalogue-import-spelling", "direct blocking observation changed");
129
+ }
130
+ else if (trace.status === "blocking-candidate") {
131
+ const old = contextual.get(call.id), adapter = adapted.get(call.id);
132
+ const partial = adapter.partialCallee.partial?.callback?.reference;
133
+ const callback = adapter.callbackPartial?.partial?.callback?.reference ?? old.callback?.reference;
134
+ const callbackMode = call.executionContext === "async-body" ? "thread-helper-callback" : call.executionContext;
135
+ check(trace.reason === "catalogue-callback-import-spelling" && [
136
+ { reference: partial, mode: call.executionContext },
137
+ { reference: callback, mode: call.category === "offload-wrapper-construction" ? "wrapper-callback-reference" : callbackMode }
138
+ ].some(item => item.reference?.status === "import-spelling" && item.reference.target === trace.target && item.mode === trace.executionContext), "callback catalogue observation has no matching reference/context");
139
+ }
140
+ else if (trace.status === "local-helper-link") {
141
+ const old = contextual.get(call.id), adapter = adapted.get(call.id);
142
+ const partial = adapter.partialCallee.partial?.callback?.reference;
143
+ const callback = adapter.callbackPartial?.partial?.callback?.reference ?? old.callback?.reference;
144
+ const callbackMode = call.executionContext === "async-body" ? "thread-helper-callback" : call.executionContext;
145
+ const references = [
146
+ { reference: old.localCallee, mode: call.executionContext }, { reference: partial, mode: call.executionContext },
147
+ { reference: adapter.decoratedCallee, mode: callbackMode },
148
+ { reference: callback, mode: call.category === "offload-wrapper-construction" ? "wrapper-callback-reference" : callbackMode }
149
+ ];
150
+ check(references.some(item => item.reference?.definition?.ownerId === trace.targetOwnerId && item.mode === trace.executionContext), "root helper link has no prior source reference");
151
+ }
152
+ }
153
+ for (const call of calls.values())
154
+ if (call.category === "blocking-call-candidate")
155
+ check(row.traces.filter(trace => trace.path.length === 1 && trace.path[0].callId === call.id).length === 1, "missing direct blocking trace");
156
+ else if (call.category === "unresolved-call")
157
+ check(row.traces.some(trace => trace.path.length === 1 && trace.path[0].callId === call.id), "missing unresolved or linked root call");
158
+ }
159
+ const expected = { ...helperCounts(report.functions), populations: Object.fromEntries([...new Set(report.functions.map(row => row.population))].sort()
160
+ .map(population => [population, helperCounts(report.functions.filter(row => row.population === population))])) };
161
+ same(report.summary, expected, "helper summary mismatch");
162
+ }
163
+ export function asyncOwnerScore(affected, eligible) {
164
+ check(Number.isSafeInteger(affected) && Number.isSafeInteger(eligible) && affected >= 0 && affected <= eligible, "invalid owner counts");
165
+ if (!eligible)
166
+ return null;
167
+ for (const band of asyncLadder)
168
+ if (100 * affected < band.percent * eligible || band.inclusive && 100 * affected === band.percent * eligible)
169
+ return band.score;
170
+ return 0;
171
+ }
172
+ export function asyncScoreSummary(rows) {
173
+ const eligible = rows.filter(row => row.eligible), affected = eligible.filter(row => row.affected).length;
174
+ const observations = eligible.flatMap(row => row.observations), unresolved = eligible.flatMap(row => row.unresolvedPaths);
175
+ return { availability: eligible.length ? "available" : "unavailable", eligibleOwners: eligible.length, affectedOwners: affected,
176
+ score: asyncOwnerScore(affected, eligible.length), affectedRate: eligible.length ? { numerator: affected, denominator: eligible.length } : null,
177
+ excludedOwnersByPopulation: histogram(rows.filter(row => !row.eligible).map(row => row.population)),
178
+ observationPathsByClassification: histogram(observations.map(item => item.classification)),
179
+ distinctScoredSourceCalls: new Set(observations.filter(item => item.classification === "scored-signal").map(item => item.path.at(-1).callId)).size,
180
+ ownersWithUnresolvedPaths: eligible.filter(row => row.unresolvedPaths.length).length,
181
+ unresolvedPathsByReason: histogram(unresolved.map(item => item.reason)) };
182
+ }
183
+ export function validateAsyncScoring(value, base, usage) {
184
+ keys(value, [...commonKeys, "helperRowsSha256"]);
185
+ const report = value;
186
+ check(report.helperRowsSha256 === fingerprint(base.functions), "helper rows hash mismatch");
187
+ if (!envelope(report, base, pythonAsyncPolicy, "experimental-python-async-scoring", "codemetrics-ai/python-async-scoring", { policy: pythonAsyncPolicy, helperConfigurationFingerprint: base.configurationFingerprint, intentPolicy: asyncIntentPolicy,
188
+ maxSourceIndexNodes: 1000000, ladder: asyncLadder, population: "selected-production-async-implementations", unresolvedTreatment: "visible-without-deduction" }))
189
+ return;
190
+ same(report.functions.map(row => row.id), base.functions.map(row => row.id), "scoring owner population/order mismatch");
191
+ const owners = new Map(usage.functions.map(row => [row.id, row]));
192
+ for (const [index, row] of report.functions.entries()) {
193
+ keys(row, [...identityKeys, "eligible", "eligibilityReason", "affected", "observations", "unresolvedPaths"]);
194
+ const original = base.functions[index];
195
+ for (const key of identityKeys)
196
+ same(row[key], original[key], "scoring owner provenance mismatch");
197
+ check(row.eligible === (row.population === "production") && row.eligibilityReason === (row.eligible ? "production-async-implementation" : "nonproduction"), "scoring eligibility mismatch");
198
+ check(Array.isArray(row.observations), "observations required");
199
+ same(row.unresolvedPaths, original.traces.filter(trace => trace.status === "unresolved"), "unresolved evidence changed");
200
+ const candidates = original.traces.filter(trace => trace.status === "blocking-candidate");
201
+ check(row.observations.length === candidates.length, "candidate population mismatch");
202
+ for (const [at, observation] of row.observations.entries()) {
203
+ keys(observation, ["path", "executionContext", "status", "reason", "target", "documentation", "classification"]);
204
+ const { documentation, classification, ...trace } = observation;
205
+ same(trace, candidates[at], "candidate path changed");
206
+ check(Array.isArray(documentation), "documentation required");
207
+ const seen = new Set();
208
+ for (const item of documentation) {
209
+ keys(item, ["file", "callId", "line", "text"]);
210
+ const step = trace.path.find(part => part.callId === item.callId && part.file === item.file);
211
+ const owner = step && owners.get(step.ownerId);
212
+ check(owner && Number.isSafeInteger(item.line) && item.line > owner.line && item.line <= owner.endLine && nonempty(item.text), "documentation source mismatch");
213
+ check(/\b(blocking|synchronous|sync-over-async|sleep)\b/i.test(item.text) && /\b(intentional(ly)?|deliberate(ly)?|required|necessary|unavoidable)\b/i.test(item.text) &&
214
+ !/\b(TODO|FIXME|HACK|XXX|not|never|avoid|remove|replace|shouldn't|mustn't|unnecessary)\b/i.test(item.text) && !/^(noqa\b|type:|pragma:|fmt:|pylint:|ruff:)/i.test(item.text), "documentation convention mismatch");
215
+ const key = fingerprint(item);
216
+ check(!seen.has(key), "duplicate rationale");
217
+ seen.add(key);
218
+ }
219
+ if (trace.executionContext !== "async-body")
220
+ check(!documentation.length && classification === "excluded-execution-context", "callback/deferred evidence cannot be scored");
221
+ else
222
+ check(classification === (documentation.length ? "documented-choice" : "scored-signal"), "classification mismatch");
223
+ }
224
+ check(row.affected === (row.eligible && row.observations.some(item => item.classification === "scored-signal")), "affected owner mismatch");
225
+ }
226
+ same(report.summary, asyncScoreSummary(report.functions), "async score/summary mismatch");
227
+ }
228
+ export function scorePythonAsync(dimension, report, complete) {
229
+ const scope = { id: pythonAsyncPolicy, coverage: "partial",
230
+ includes: ["selected-production-async-implementations", "blocking-import-catalogue", "bounded-eager-local-helpers", "documented-choice-exclusions", "explicit-unresolved-paths"],
231
+ excludes: ["test-support-generated-scoring", "runtime-performance", "complete-blocking-detection", "resolved-receiver-types", "cross-file-call-graphs", "business-correctness"] };
232
+ const details = complete && report?.status === "complete" ? asyncScoreSummary(report.functions) : null;
233
+ if (details?.score === null || !details)
234
+ return { ...dimension, scope,
235
+ status: complete ? "skipped" : "failed", basis: complete ? "Async/Blocking Usage unavailable: no selected production async implementations." : "Python analysis incomplete; no async score is usable." };
236
+ const findings = report.functions.filter(row => row.observations.length).map(row => ({
237
+ category: "pythonAsyncBlockingReview", ruleId: pythonAsyncPolicy, fingerprint: hash(`${pythonAsyncPolicy}:${row.id}`),
238
+ severity: row.affected ? "warning" : "info", confidence: "medium", file: row.file,
239
+ line: row.observations[0].path[0].line, member: row.member,
240
+ message: row.affected ? "Supported blocking-call syntax appears in this async owner's eager path. Inspect required behavior and source context before changing execution."
241
+ : "Blocking-call syntax is retained as excluded context or documented intent; no score deduction.",
242
+ observations: { ownerId: row.id, population: row.population, eligible: row.eligible, affected: row.affected,
243
+ paths: row.observations, classification: row.affected ? "actionableSignal" : "reviewLead" }
244
+ }));
245
+ const decision = firstMatch(pythonAsyncPolicy, { eligibleOwners: details.eligibleOwners, affectedOwners: details.affectedOwners,
246
+ ownersWithUnresolvedPaths: details.ownersWithUnresolvedPaths, unresolvedPathsByReason: details.unresolvedPathsByReason,
247
+ ladder: asyncLadder, counting: "distinct-selected-production-async-owner", intentPolicy: asyncIntentPolicy }, [{ id: "affectedOwnerRate", condition: "nonempty selected production async implementation population", matched: true, score: details.score, categories: ["pythonAsyncBlockingReview"] }]);
248
+ return { ...dimension, ...scored(decision, "Partial static blocking-catalogue usage score. Unresolved paths are not nonblocking evidence; score does not measure runtime performance.", findings, details, finding => finding.observations.affected === true), scope };
249
+ }
@@ -0,0 +1,56 @@
1
+ import type { SourceFacts } from "#source-facts/contract.mjs";
2
+ import type { Evidence } from "./evidence.js";
3
+ import type { PythonDecomposition } from "./python-decomposition.js";
4
+ export declare const asyncUsagePolicy = "python/async-owned-calls-review-v1";
5
+ interface Binding {
6
+ syntax: string | null;
7
+ target: string | null;
8
+ status: string;
9
+ reason: string;
10
+ }
11
+ interface Callback {
12
+ kind: string;
13
+ binding: Binding | null;
14
+ evaluation?: string;
15
+ }
16
+ interface Call {
17
+ id: string;
18
+ line: number;
19
+ column: number;
20
+ endLine: number;
21
+ endColumn: number;
22
+ executionContext: string;
23
+ category: string;
24
+ binding: Binding;
25
+ directlyAwaited: boolean;
26
+ callback: Callback | null;
27
+ disposition: string;
28
+ }
29
+ interface Owner {
30
+ id: string;
31
+ file: string;
32
+ member: string;
33
+ line: number;
34
+ endLine: number;
35
+ population: string;
36
+ sourceSha256: string;
37
+ functionKind: string;
38
+ disposition: string;
39
+ status: string;
40
+ reason: string | null;
41
+ measurement: {
42
+ kind: string;
43
+ awaitExpressions: Record<string, number>;
44
+ calls: Call[];
45
+ } | null;
46
+ }
47
+ export interface PythonAsyncUsage extends Omit<PythonDecomposition, "configuration" | "functions"> {
48
+ configuration: Record<string, unknown>;
49
+ functions: Owner[];
50
+ }
51
+ /** Validate identity, catalogue coherence and accounting, not runtime resolution or independent AST semantics. */
52
+ export declare function validateAsyncUsage(value: unknown, facts: SourceFacts, subject: Evidence["subject"], ids: {
53
+ runId: string;
54
+ auditId: string;
55
+ }, decomposition: PythonDecomposition): asserts value is PythonAsyncUsage;
56
+ export {};