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.
- package/README.md +51 -5
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.js +11 -5
- package/dist/cli.js +7 -3
- package/dist/dependency-evidence.d.ts +5 -0
- package/dist/dependency-evidence.js +30 -0
- package/dist/dependency-inventory.d.ts +67 -0
- package/dist/dependency-inventory.js +203 -0
- package/dist/dependency-metadata.d.ts +65 -0
- package/dist/dependency-metadata.js +125 -0
- package/dist/dependency-pnpm.d.ts +4 -0
- package/dist/dependency-pnpm.js +237 -0
- package/dist/dependency-spec.d.ts +13 -0
- package/dist/dependency-spec.js +30 -0
- package/dist/error-handling-scoring.d.ts +1 -0
- package/dist/error-handling-scoring.js +15 -0
- package/dist/error-handling.d.ts +4 -1
- package/dist/error-handling.js +50 -17
- package/dist/evidence-reader.js +3 -1
- package/dist/evidence-tools.js +4 -0
- package/dist/failure-contracts.d.ts +19 -0
- package/dist/failure-contracts.js +261 -0
- package/dist/function-scoring.d.ts +2 -1
- package/dist/function-scoring.js +13 -24
- package/dist/handler-disposition.d.ts +9 -0
- package/dist/handler-disposition.js +156 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/python/ASYNC_ADAPTERS.md +38 -0
- package/dist/python/ASYNC_CONTEXT.md +58 -0
- package/dist/python/ASYNC_SCORING.md +174 -0
- package/dist/python/ASYNC_USAGE.md +62 -0
- package/dist/python/BEHAVIOR_REVIEW.md +84 -0
- package/dist/python/COVERAGE.md +96 -0
- package/dist/python/DECOMPOSITION.md +46 -0
- package/dist/python/DUPLICATION.md +36 -0
- package/dist/python/ERROR_HANDLING.md +45 -0
- package/dist/python/ERROR_HANDLING_CONTEXT.md +61 -0
- package/dist/python/ERROR_HANDLING_OPERATIONS.md +54 -0
- package/dist/python/ERROR_HANDLING_TRANSFERS.md +64 -0
- package/dist/python/METHOD_COMPLEXITY.md +46 -0
- package/dist/python/README.md +162 -0
- package/dist/python/analysis.py +54 -0
- package/dist/python/async_adapters.py +243 -0
- package/dist/python/async_bindings.py +180 -0
- package/dist/python/async_context.py +143 -0
- package/dist/python/async_helper_paths.py +186 -0
- package/dist/python/async_provenance.py +129 -0
- package/dist/python/async_scoring.py +203 -0
- package/dist/python/async_usage.py +244 -0
- package/dist/python/coverage_collect.py +121 -0
- package/dist/python/coverage_provenance.py +77 -0
- package/dist/python/coverage_review.py +273 -0
- package/dist/python/coverage_worker.py +46 -0
- package/dist/python/decomposition.py +199 -0
- package/dist/python/duplication.py +177 -0
- package/dist/python/error_handling.py +249 -0
- package/dist/python/error_handling_context.py +294 -0
- package/dist/python/error_handling_operations.py +228 -0
- package/dist/python/error_handling_transfers.py +211 -0
- package/dist/python/review_cli.py +36 -0
- package/dist/python/source_facts.py +293 -0
- package/dist/python/source_selection.py +157 -0
- package/dist/python-analyzer.d.ts +21 -0
- package/dist/python-analyzer.js +123 -0
- package/dist/python-async-adapters.d.ts +67 -0
- package/dist/python-async-adapters.js +197 -0
- package/dist/python-async-context.d.ts +73 -0
- package/dist/python-async-context.js +181 -0
- package/dist/python-async-scoring.d.ts +89 -0
- package/dist/python-async-scoring.js +249 -0
- package/dist/python-async-usage.d.ts +56 -0
- package/dist/python-async-usage.js +143 -0
- package/dist/python-cli.d.ts +2 -0
- package/dist/python-cli.js +53 -0
- package/dist/python-collect-cli.d.ts +2 -0
- package/dist/python-collect-cli.js +28 -0
- package/dist/python-complexity.d.ts +18 -0
- package/dist/python-complexity.js +36 -0
- package/dist/python-coverage.d.ts +63 -0
- package/dist/python-coverage.js +173 -0
- package/dist/python-decomposition.d.ts +56 -0
- package/dist/python-decomposition.js +102 -0
- package/dist/python-duplication.d.ts +65 -0
- package/dist/python-duplication.js +89 -0
- package/dist/python-error-context.d.ts +84 -0
- package/dist/python-error-context.js +165 -0
- package/dist/python-error-handling.d.ts +56 -0
- package/dist/python-error-handling.js +120 -0
- package/dist/python-error-operations.d.ts +44 -0
- package/dist/python-error-operations.js +155 -0
- package/dist/python-error-transfers.d.ts +38 -0
- package/dist/python-error-transfers.js +125 -0
- package/dist/python-evidence.d.ts +23 -0
- package/dist/python-evidence.js +255 -0
- package/dist/score-arithmetic.d.ts +1 -12
- package/dist/score-arithmetic.js +1 -40
- package/dist/shared-source-facts/arithmetic.d.mts +9 -0
- package/dist/shared-source-facts/arithmetic.mjs +32 -0
- package/dist/shared-source-facts/contract.d.mts +35 -0
- package/dist/shared-source-facts/contract.mjs +95 -0
- package/dist/shared-source-facts/population.d.mts +12 -0
- package/dist/shared-source-facts/population.mjs +41 -0
- package/dist/source-facts.d.ts +4 -0
- package/dist/source-facts.js +16 -0
- package/package.json +15 -6
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
export declare function standardConsole(call: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
3
|
+
/** Source-backed contracts, not name-based exemptions or a runtime data-flow proof. */
|
|
4
|
+
export declare class FailureContracts {
|
|
5
|
+
private readonly checker;
|
|
6
|
+
private readonly selected;
|
|
7
|
+
private readonly references;
|
|
8
|
+
private readonly writes;
|
|
9
|
+
private readonly local;
|
|
10
|
+
constructor(checker: ts.TypeChecker, sources: readonly ts.SourceFile[], selected: readonly ts.SourceFile[], root: string);
|
|
11
|
+
forwardsCaughtError(call: ts.CallExpression): boolean;
|
|
12
|
+
explicitResult(expression: ts.Expression | undefined, depth?: number): boolean;
|
|
13
|
+
reports(call: ts.CallExpression): boolean;
|
|
14
|
+
private reachesConsole;
|
|
15
|
+
guardedUndefinedResult(body: ts.ConciseBody): boolean;
|
|
16
|
+
private commonJsFile;
|
|
17
|
+
private guardedCallResult;
|
|
18
|
+
private truthyGuard;
|
|
19
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { exclusion } from "./discovery.js";
|
|
4
|
+
import { unwrapHandler } from "./handler-resolution.js";
|
|
5
|
+
export function standardConsole(call, checker) {
|
|
6
|
+
const member = call.expression;
|
|
7
|
+
if (!ts.isPropertyAccessExpression(member) || !["error", "warn", "info", "log", "debug", "trace"].includes(member.name.text) ||
|
|
8
|
+
!ts.isIdentifier(member.expression) || member.expression.text !== "console" || !call.arguments.length || call.questionDotToken || member.questionDotToken)
|
|
9
|
+
return false;
|
|
10
|
+
const declarations = checker.getSymbolAtLocation(member.expression)?.declarations ?? [];
|
|
11
|
+
return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile &&
|
|
12
|
+
/(?:lib\.dom\.d\.ts|[/\\]@types[/\\]node[/\\](?:web-globals[/\\])?(?:console|globals)\.d\.ts)$/.test(declaration.getSourceFile().fileName));
|
|
13
|
+
}
|
|
14
|
+
const descendants = (node, match) => match(node) || (ts.forEachChild(node, child => descendants(child, match)) ?? false);
|
|
15
|
+
function enclosingFunction(node) {
|
|
16
|
+
for (let at = node.parent; at; at = at.parent)
|
|
17
|
+
if (ts.isFunctionLike(at))
|
|
18
|
+
return at;
|
|
19
|
+
}
|
|
20
|
+
function documentation(node) {
|
|
21
|
+
return (ts.getLeadingCommentRanges(node.getSourceFile().text, node.getFullStart()) ?? [])
|
|
22
|
+
.map(range => node.getSourceFile().text.slice(range.pos, range.end)).join("\n");
|
|
23
|
+
}
|
|
24
|
+
/** Source-backed contracts, not name-based exemptions or a runtime data-flow proof. */
|
|
25
|
+
export class FailureContracts {
|
|
26
|
+
checker;
|
|
27
|
+
selected;
|
|
28
|
+
references = new Map();
|
|
29
|
+
writes = new Set();
|
|
30
|
+
local = new Set();
|
|
31
|
+
constructor(checker, sources, selected, root) {
|
|
32
|
+
this.checker = checker;
|
|
33
|
+
this.selected = selected;
|
|
34
|
+
for (const source of sources) {
|
|
35
|
+
const relative = path.relative(root, source.fileName).replaceAll("\\", "/");
|
|
36
|
+
if (source.isDeclarationFile || relative.startsWith("../") || exclusion(relative))
|
|
37
|
+
continue;
|
|
38
|
+
this.local.add(source);
|
|
39
|
+
const visit = (node) => {
|
|
40
|
+
if (ts.isIdentifier(node)) {
|
|
41
|
+
const symbol = ts.isShorthandPropertyAssignment(node.parent)
|
|
42
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent) : checker.getSymbolAtLocation(node);
|
|
43
|
+
if (symbol) {
|
|
44
|
+
const refs = this.references.get(symbol) ?? [];
|
|
45
|
+
refs.push(node);
|
|
46
|
+
this.references.set(symbol, refs);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
|
|
50
|
+
const record = (target) => {
|
|
51
|
+
if (ts.isIdentifier(target) || ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) {
|
|
52
|
+
const symbol = checker.getSymbolAtLocation(target);
|
|
53
|
+
if (symbol)
|
|
54
|
+
this.writes.add(symbol);
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
ts.forEachChild(target, record);
|
|
58
|
+
};
|
|
59
|
+
record(node.left);
|
|
60
|
+
}
|
|
61
|
+
if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
62
|
+
[ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
|
|
63
|
+
const symbol = checker.getSymbolAtLocation(node.operand);
|
|
64
|
+
if (symbol)
|
|
65
|
+
this.writes.add(symbol);
|
|
66
|
+
}
|
|
67
|
+
ts.forEachChild(node, visit);
|
|
68
|
+
};
|
|
69
|
+
visit(source);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
forwardsCaughtError(call) {
|
|
73
|
+
if (call.questionDotToken || !ts.isIdentifier(call.expression) || !call.arguments.length)
|
|
74
|
+
return false;
|
|
75
|
+
const target = this.checker.getSymbolAtLocation(call.expression), error = unwrapHandler(call.arguments[0]);
|
|
76
|
+
const declaration = target?.declarations?.[0];
|
|
77
|
+
if (!target || this.writes.has(target) || !declaration || !ts.isParameter(declaration) || declaration.initializer ||
|
|
78
|
+
declaration.getSourceFile() !== call.getSourceFile() || call.pos < declaration.parent.pos || call.end > declaration.parent.end)
|
|
79
|
+
return false;
|
|
80
|
+
const caughtSymbol = (symbol) => {
|
|
81
|
+
const binding = symbol?.declarations?.[0];
|
|
82
|
+
return !!symbol && !!binding && ts.isVariableDeclaration(binding) && ts.isCatchClause(binding.parent) &&
|
|
83
|
+
!this.writes.has(symbol) && call.pos >= binding.parent.block.pos && call.end <= binding.parent.block.end;
|
|
84
|
+
};
|
|
85
|
+
// Only plain payload expressions, never an unrelated call taking the error.
|
|
86
|
+
const payload = (node) => {
|
|
87
|
+
if (ts.isIdentifier(node) && caughtSymbol(this.checker.getSymbolAtLocation(node)))
|
|
88
|
+
return true;
|
|
89
|
+
if (ts.isPropertyAccessExpression(node))
|
|
90
|
+
return payload(node.expression);
|
|
91
|
+
if (ts.isObjectLiteralExpression(node))
|
|
92
|
+
return node.properties.some(property => ts.isPropertyAssignment(property) ? payload(property.initializer) : ts.isShorthandPropertyAssignment(property) &&
|
|
93
|
+
caughtSymbol(this.checker.getShorthandAssignmentValueSymbol(property)));
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
return payload(error);
|
|
97
|
+
}
|
|
98
|
+
explicitResult(expression, depth = 0) {
|
|
99
|
+
if (!expression || depth > 4)
|
|
100
|
+
return false;
|
|
101
|
+
expression = unwrapHandler(expression);
|
|
102
|
+
if (ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression) || ts.isNewExpression(expression))
|
|
103
|
+
return true;
|
|
104
|
+
if (!ts.isIdentifier(expression))
|
|
105
|
+
return false;
|
|
106
|
+
const symbol = this.checker.getSymbolAtLocation(expression), declarations = symbol?.declarations ?? [];
|
|
107
|
+
if (!symbol || this.writes.has(symbol) || declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]))
|
|
108
|
+
return false;
|
|
109
|
+
const declaration = declarations[0];
|
|
110
|
+
if (!ts.isVariableDeclarationList(declaration.parent) || !(declaration.parent.flags & ts.NodeFlags.Const) || !declaration.initializer)
|
|
111
|
+
return false;
|
|
112
|
+
const value = unwrapHandler(declaration.initializer);
|
|
113
|
+
return ts.isLiteralExpression(value) || [ts.SyntaxKind.NullKeyword, ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(value.kind) || this.explicitResult(value, depth + 1);
|
|
114
|
+
}
|
|
115
|
+
reports(call) {
|
|
116
|
+
return this.reachesConsole(call, new Set(), 0);
|
|
117
|
+
}
|
|
118
|
+
reachesConsole(call, seen, depth, payloadPositions = new Set(call.arguments.map((_, index) => index))) {
|
|
119
|
+
if (call.questionDotToken || (ts.isPropertyAccessExpression(call.expression) && call.expression.questionDotToken))
|
|
120
|
+
return false;
|
|
121
|
+
if (standardConsole(call, this.checker))
|
|
122
|
+
return true;
|
|
123
|
+
const symbol = this.checker.getSymbolAtLocation(call.expression);
|
|
124
|
+
if (symbol && this.writes.has(symbol))
|
|
125
|
+
return false;
|
|
126
|
+
if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)) {
|
|
127
|
+
const receiver = this.checker.getSymbolAtLocation(call.expression.expression);
|
|
128
|
+
if (receiver && this.writes.has(receiver))
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const declaration = this.checker.getResolvedSignature(call)?.declaration;
|
|
132
|
+
if (!declaration || depth >= 3 || seen.has(declaration) || !this.local.has(declaration.getSourceFile()) ||
|
|
133
|
+
!(ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isArrowFunction(declaration)) || !declaration.body)
|
|
134
|
+
return false;
|
|
135
|
+
seen = new Set(seen).add(declaration);
|
|
136
|
+
const parameters = new Set(declaration.parameters.filter((parameter, index) => payloadPositions.has(index) || parameter.dotDotDotToken && [...payloadPositions].some(position => position >= index))
|
|
137
|
+
.map(parameter => this.checker.getSymbolAtLocation(parameter.name)).filter(symbol => symbol && !this.writes.has(symbol)));
|
|
138
|
+
const declaredLogger = /\b(?:log|write)\b[^\n]*\b(?:message|exception|error|warn)\b/i.test(documentation(declaration));
|
|
139
|
+
// Ordinary wrappers require an unconditional forwarding call. A documented
|
|
140
|
+
// logger may have runtime level filters; prove its payload reaches a console sink.
|
|
141
|
+
const candidates = [];
|
|
142
|
+
const visit = (node) => {
|
|
143
|
+
if (node !== declaration.body && ts.isFunctionLike(node))
|
|
144
|
+
return;
|
|
145
|
+
if (ts.isCallExpression(node))
|
|
146
|
+
candidates.push(node);
|
|
147
|
+
ts.forEachChild(node, visit);
|
|
148
|
+
};
|
|
149
|
+
if (declaredLogger)
|
|
150
|
+
visit(declaration.body);
|
|
151
|
+
else if (ts.isBlock(declaration.body)) {
|
|
152
|
+
for (const statement of declaration.body.statements) {
|
|
153
|
+
if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression))
|
|
154
|
+
candidates.push(statement.expression);
|
|
155
|
+
else if (ts.isReturnStatement(statement) && statement.expression && ts.isCallExpression(statement.expression)) {
|
|
156
|
+
candidates.push(statement.expression);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
else if (!ts.isVariableStatement(statement) && !ts.isEmptyStatement(statement))
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (ts.isCallExpression(declaration.body))
|
|
164
|
+
candidates.push(declaration.body);
|
|
165
|
+
return candidates.some(candidate => {
|
|
166
|
+
const forwarded = new Set(candidate.arguments.flatMap((arg, index) => descendants(arg, node => ts.isIdentifier(node) && parameters.has(this.checker.getSymbolAtLocation(node))) ? [index] : []));
|
|
167
|
+
return forwarded.size > 0 && this.reachesConsole(candidate, seen, depth + 1, forwarded);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
guardedUndefinedResult(body) {
|
|
171
|
+
const fn = enclosingFunction(body);
|
|
172
|
+
if (!fn || !ts.isFunctionDeclaration(fn) || !fn.name || ts.getCombinedModifierFlags(fn) & ts.ModifierFlags.Export)
|
|
173
|
+
return false;
|
|
174
|
+
const symbol = this.checker.getSymbolAtLocation(fn.name);
|
|
175
|
+
if (!symbol || this.writes.has(symbol))
|
|
176
|
+
return false;
|
|
177
|
+
const module = this.checker.getSymbolAtLocation(fn.getSourceFile());
|
|
178
|
+
if (!module && !this.commonJsFile(fn.getSourceFile()) && !enclosingFunction(fn))
|
|
179
|
+
return false;
|
|
180
|
+
if (module && this.checker.getExportsOfModule(module).some(exported => (exported.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(exported) : exported) === symbol))
|
|
181
|
+
return false;
|
|
182
|
+
const uses = (this.references.get(symbol) ?? []).filter(node => node !== fn.name);
|
|
183
|
+
return uses.length > 0 && uses.every(node => this.selected.includes(node.getSourceFile()) &&
|
|
184
|
+
ts.isCallExpression(node.parent) && node.parent.expression === node && this.guardedCallResult(node.parent));
|
|
185
|
+
}
|
|
186
|
+
commonJsFile(source) {
|
|
187
|
+
const exportRoot = (node) => {
|
|
188
|
+
if (!ts.isPropertyAccessExpression(node))
|
|
189
|
+
return;
|
|
190
|
+
if (ts.isIdentifier(node.expression)) {
|
|
191
|
+
const base = node.expression;
|
|
192
|
+
return base.text === "exports" || base.text === "module" && node.name.text === "exports" ? base : undefined;
|
|
193
|
+
}
|
|
194
|
+
return exportRoot(node.expression);
|
|
195
|
+
};
|
|
196
|
+
const inferredExport = (node) => {
|
|
197
|
+
// Depending on the assignments present, the JS binder uses either the
|
|
198
|
+
// assignment or its root identifier as the inferred module declaration.
|
|
199
|
+
while (ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)
|
|
200
|
+
node = node.parent;
|
|
201
|
+
if (ts.isBinaryExpression(node.parent) && node.parent.left === node)
|
|
202
|
+
node = node.parent;
|
|
203
|
+
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && !!exportRoot(node.left);
|
|
204
|
+
};
|
|
205
|
+
return source.statements.some(statement => {
|
|
206
|
+
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) ||
|
|
207
|
+
statement.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
208
|
+
return false;
|
|
209
|
+
const base = exportRoot(statement.expression.left);
|
|
210
|
+
if (!base)
|
|
211
|
+
return false;
|
|
212
|
+
const declarations = this.checker.getSymbolAtLocation(base)?.declarations ?? [];
|
|
213
|
+
// TypeScript binds bare CommonJS `module` to its export assignment when
|
|
214
|
+
// Node declarations are absent; that inferred declaration is not shadowing.
|
|
215
|
+
return declarations.every(declaration => inferredExport(declaration) ||
|
|
216
|
+
declaration.getSourceFile().isDeclarationFile && /[/\\]@types[/\\]node[/\\]/.test(declaration.getSourceFile().fileName));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
guardedCallResult(call) {
|
|
220
|
+
const parent = call.parent;
|
|
221
|
+
let identifier, statement;
|
|
222
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
223
|
+
identifier = parent.name;
|
|
224
|
+
statement = parent.parent.parent;
|
|
225
|
+
}
|
|
226
|
+
else if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(parent.left)) {
|
|
227
|
+
identifier = parent.left;
|
|
228
|
+
statement = parent.parent;
|
|
229
|
+
}
|
|
230
|
+
else
|
|
231
|
+
return false;
|
|
232
|
+
if (!ts.isBlock(statement.parent))
|
|
233
|
+
return false;
|
|
234
|
+
const next = statement.parent.statements[statement.parent.statements.indexOf(statement) + 1];
|
|
235
|
+
const symbol = this.checker.getSymbolAtLocation(identifier);
|
|
236
|
+
if (!symbol || !next || !ts.isIfStatement(next) || !this.truthyGuard(next.expression, symbol))
|
|
237
|
+
return false;
|
|
238
|
+
return (this.references.get(symbol) ?? []).every(node => {
|
|
239
|
+
if (ts.isVariableDeclaration(node.parent) && node.parent.name === node)
|
|
240
|
+
return true;
|
|
241
|
+
if (ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
|
242
|
+
return ts.isCallExpression(node.parent.right) && this.checker.getSymbolAtLocation(node.parent.right.expression) === this.checker.getSymbolAtLocation(call.expression) &&
|
|
243
|
+
node.parent.parent.parent === statement.parent;
|
|
244
|
+
if (ts.isPrefixUnaryExpression(node.parent) || ts.isPostfixUnaryExpression(node.parent))
|
|
245
|
+
return false;
|
|
246
|
+
for (let at = node; at.parent && !ts.isFunctionLike(at.parent); at = at.parent) {
|
|
247
|
+
const parent = at.parent;
|
|
248
|
+
if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && this.truthyGuard(parent.left, symbol))
|
|
249
|
+
return true;
|
|
250
|
+
if (ts.isIfStatement(parent) && (parent.expression === at || parent.thenStatement === at) && this.truthyGuard(parent.expression, symbol))
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
truthyGuard(expression, symbol) {
|
|
257
|
+
expression = unwrapHandler(expression);
|
|
258
|
+
return ts.isIdentifier(expression) ? this.checker.getSymbolAtLocation(expression) === symbol :
|
|
259
|
+
ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && this.truthyGuard(expression.left, symbol);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Dimension, type Finding } from "./evidence.js";
|
|
2
2
|
import type { FunctionMeasurement } from "./function-measurements.js";
|
|
3
|
-
|
|
3
|
+
import { complexityAnchors } from "#source-facts/population.mjs";
|
|
4
|
+
export { complexityAnchors };
|
|
4
5
|
export declare const maintainabilityAnchors: readonly [readonly [40, 0], readonly [52, 2], readonly [58, 4], readonly [65, 6], readonly [70, 8], readonly [75, 10]];
|
|
5
6
|
export declare function scoreFunctions(functions: FunctionMeasurement[], dimension: "codeQuality" | "maintainability", findings?: Finding[]): Dimension;
|
package/dist/function-scoring.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { scored } from "./evidence.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { aggregatePopulation, complexityPopulation, complexityAnchors } from "#source-facts/population.mjs";
|
|
3
|
+
import { functionSourceFacts } from "./source-facts.js";
|
|
4
|
+
export { complexityAnchors };
|
|
4
5
|
export const maintainabilityAnchors = [[40, 0], [52, 2], [58, 4], [65, 6], [70, 8], [75, 10]];
|
|
5
6
|
export function scoreFunctions(functions, dimension, findings = []) {
|
|
6
7
|
const complexity = dimension === "codeQuality";
|
|
@@ -11,29 +12,17 @@ export function scoreFunctions(functions, dimension, findings = []) {
|
|
|
11
12
|
excludes: ["module-and-field-initializers", "parameter-initializers", "runtime-behavior", complexity ? "decomposition" : "change-cost"] };
|
|
12
13
|
if (!functions.length)
|
|
13
14
|
return { status: "skipped", basis: "No implemented functions; function score is unmeasured.", findings, scope };
|
|
14
|
-
const
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const weakWeight = restCount ? fraction(2n, 5n * BigInt(weakCount)) : fraction(1n, BigInt(weakCount));
|
|
20
|
-
const restWeight = restCount ? fraction(3n, 5n * BigInt(restCount)) : decimal(0);
|
|
21
|
-
const losses = { weakest: decimal(0), remainder: decimal(0) };
|
|
22
|
-
const contributions = ordered.map(({ item, score }, index) => {
|
|
23
|
-
const group = index < weakCount ? "weakest" : "remainder";
|
|
24
|
-
const weight = index < weakCount ? weakWeight : restWeight;
|
|
25
|
-
const weightedLoss = multiply(subtract(decimal(10), score), weight);
|
|
26
|
-
losses[group] = add(losses[group], weightedLoss);
|
|
27
|
-
return { ...item, group, individualScore: numeric(score), weightNumerator: Number(weight.numerator),
|
|
28
|
-
weightDenominator: Number(weight.denominator), weightedScoreLoss: numeric(weightedLoss) };
|
|
29
|
-
});
|
|
30
|
-
const total = subtract(decimal(10), add(losses.weakest, losses.remainder));
|
|
15
|
+
const population = complexity ? complexityPopulation(functionSourceFacts(functions))
|
|
16
|
+
: aggregatePopulation(functions.map(item => ({ id: item.id, value: measured(item) })), anchors, false, Math.ceil(functions.length / 5));
|
|
17
|
+
const { weakCount, restCount, losses } = population;
|
|
18
|
+
const byFunctionId = new Map(functions.map(item => [item.id, item]));
|
|
19
|
+
const contributions = population.contributions.map(({ id, ...contribution }) => ({ ...byFunctionId.get(id), ...contribution }));
|
|
31
20
|
const decision = { version: 1, policy: `javascript-typescript/${dimension}/owned-functions-v2`, operation: "deductions",
|
|
32
|
-
finalScore:
|
|
21
|
+
finalScore: population.score, inputs: { baseline: 10, eligibleFunctions: functions.length, weakCount, restCount,
|
|
33
22
|
anchors, aggregation: complexity ? "worst-40-remainder-60" : "weakest-quintile-40-remainder-60",
|
|
34
|
-
rounding: "decimal-rational-half-up-one-decimal", unroundedScore:
|
|
35
|
-
steps: (restCount ? ["weakest", "remainder"] : ["weakest"]).map(group => ({ id: group, kind: "component", score:
|
|
36
|
-
disposition: "applied", inputs: { weightedScoreLoss:
|
|
23
|
+
rounding: "decimal-rational-half-up-one-decimal", unroundedScore: population.unroundedScore },
|
|
24
|
+
steps: (restCount ? ["weakest", "remainder"] : ["weakest"]).map(group => ({ id: group, kind: "component", score: losses[group],
|
|
25
|
+
disposition: "applied", inputs: { weightedScoreLoss: losses[group], functionCount: group === "weakest" ? weakCount : restCount },
|
|
37
26
|
findingCategories: complexity ? ["highFunctionComplexity"] : [] })), findingEffects: [] };
|
|
38
27
|
const result = scored(decision, `${complexity ? "Method complexity: one worst function" : "Maintainability: weakest ceil(N/5) functions"} receives 40%; remaining functions receive 60%. Single-function scopes use their own score. JS/TS remains uncalibrated across ecosystems.`, findings, { ...decision.inputs, measurement: "owned-function-body-v2-type-erasure", functionContributions: contributions, topOffenders: contributions.slice(0, 5) });
|
|
39
28
|
// Findings identify population inputs, never additional independent penalties.
|
|
@@ -44,7 +33,7 @@ export function scoreFunctions(functions, dimension, findings = []) {
|
|
|
44
33
|
effect: group ? "policyInput" : "noAdditionalReduction", stepIds: group ? [group] : [] };
|
|
45
34
|
});
|
|
46
35
|
return { ...result, scope, ...(complexity ? { displayName: "Method Complexity", componentDetails: {
|
|
47
|
-
methodComplexity: { score: result.score, eligibleFunctions:
|
|
36
|
+
methodComplexity: { score: result.score, eligibleFunctions: functions.length, topOffenders: contributions.slice(0, 5) },
|
|
48
37
|
decomposition: { score: null, status: "unsupported", description: "JS/TS decomposition is not measured by this policy." }
|
|
49
38
|
} } : {}) };
|
|
50
39
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { FailureContracts } from "./failure-contracts.js";
|
|
3
|
+
export type DispositionKind = "propagated" | "failureResult" | "reported" | "fallback" | "documented" | "unexplainedSwallowing" | "unknown";
|
|
4
|
+
export interface HandlerDisposition {
|
|
5
|
+
classification: DispositionKind;
|
|
6
|
+
reason: string;
|
|
7
|
+
comments?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function classifyDisposition(body: ts.ConciseBody, checker: ts.TypeChecker, empty: boolean, comments: string[], use: ts.Node, contracts: FailureContracts): HandlerDisposition;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { unwrapHandler } from "./handler-resolution.js";
|
|
3
|
+
const result = (classification, reason) => ({ classification, reason });
|
|
4
|
+
// These are declarations of intent, not a judgment about whether the rationale is good.
|
|
5
|
+
const intentional = /\b(?:ignor(?:e|ed|ing)|swallow(?:ed|ing)?|discard(?:ed|ing)?|optional|best[- ]effort|does nothing|no[- ]op|intention(?:al|ally))\b/i;
|
|
6
|
+
function leadingComments(node) {
|
|
7
|
+
const source = node.getSourceFile();
|
|
8
|
+
return (ts.getLeadingCommentRanges(source.text, node.getFullStart()) ?? [])
|
|
9
|
+
.map(range => source.text.slice(range.pos, range.end)).filter(text => intentional.test(text));
|
|
10
|
+
}
|
|
11
|
+
function declaredIntent(body, checker) {
|
|
12
|
+
const owner = body.parent;
|
|
13
|
+
if (ts.isCatchClause(owner))
|
|
14
|
+
return [...leadingComments(owner), ...leadingComments(owner.parent)];
|
|
15
|
+
if (ts.isFunctionDeclaration(owner)) {
|
|
16
|
+
const declarations = owner.name ? checker.getSymbolAtLocation(owner.name)?.declarations ?? [owner] : [owner];
|
|
17
|
+
return declarations.flatMap(leadingComments);
|
|
18
|
+
}
|
|
19
|
+
if ((ts.isArrowFunction(owner) || ts.isFunctionExpression(owner)) && ts.isVariableDeclaration(owner.parent)) {
|
|
20
|
+
const list = owner.parent.parent;
|
|
21
|
+
// A comment on a multi-binding declaration cannot identify one handler's intent.
|
|
22
|
+
if (ts.isVariableDeclarationList(list) && list.declarations.length === 1)
|
|
23
|
+
return leadingComments(list.parent);
|
|
24
|
+
}
|
|
25
|
+
return leadingComments(owner);
|
|
26
|
+
}
|
|
27
|
+
function explicitValue(expression) {
|
|
28
|
+
if (!expression)
|
|
29
|
+
return false;
|
|
30
|
+
const node = unwrapHandler(expression);
|
|
31
|
+
// No names, calls, spreads or property reads: their value/side effects need more flow analysis.
|
|
32
|
+
if (ts.isLiteralExpression(node) || [ts.SyntaxKind.NullKeyword, ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword].includes(node.kind))
|
|
33
|
+
return true;
|
|
34
|
+
if (ts.isPrefixUnaryExpression(node))
|
|
35
|
+
return ts.isNumericLiteral(node.operand);
|
|
36
|
+
if (ts.isArrayLiteralExpression(node))
|
|
37
|
+
return node.elements.every(element => explicitValue(element));
|
|
38
|
+
return ts.isObjectLiteralExpression(node) && node.properties.every(property => ts.isPropertyAssignment(property) &&
|
|
39
|
+
!ts.isComputedPropertyName(property.name) && explicitValue(property.initializer));
|
|
40
|
+
}
|
|
41
|
+
function statementOutcome(statement, checker, contracts) {
|
|
42
|
+
if (ts.isThrowStatement(statement))
|
|
43
|
+
return result("propagated", "Unconditional throw on the failure path.");
|
|
44
|
+
if (ts.isReturnStatement(statement))
|
|
45
|
+
return explicitValue(statement.expression) || contracts.explicitResult(statement.expression)
|
|
46
|
+
? result("failureResult", "Returns an explicit value/container on the failure path; suitability and caller correctness are not judged.")
|
|
47
|
+
: result("unknown", "Return value or caller contract requires additional analysis.");
|
|
48
|
+
if (ts.isBlock(statement))
|
|
49
|
+
return blockOutcome(statement.statements, checker, contracts);
|
|
50
|
+
if (ts.isIfStatement(statement)) {
|
|
51
|
+
const yes = statementOutcome(statement.thenStatement, checker, contracts);
|
|
52
|
+
const no = statement.elseStatement && statementOutcome(statement.elseStatement, checker, contracts);
|
|
53
|
+
if (yes && no && ![yes.classification, no.classification].includes("unknown"))
|
|
54
|
+
return result(yes.classification === no.classification ? yes.classification : "failureResult", "Both branches have an explicit failure disposition.");
|
|
55
|
+
return result("unknown", "Conditional failure path is not fully resolved.");
|
|
56
|
+
}
|
|
57
|
+
if (ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression)) {
|
|
58
|
+
if (contracts.forwardsCaughtError(statement.expression))
|
|
59
|
+
return result("propagated", "Passes the caught error to a caller-supplied callback; consumer correctness is outside scope.");
|
|
60
|
+
if (contracts.reports(statement.expression))
|
|
61
|
+
return result("reported", "Standard console output or a resolved source-backed logging wrapper; runtime filtering is outside scope.");
|
|
62
|
+
}
|
|
63
|
+
if (ts.isExpressionStatement(statement) || ts.isVariableStatement(statement) || ts.isEmptyStatement(statement) ||
|
|
64
|
+
ts.isFunctionDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement))
|
|
65
|
+
return;
|
|
66
|
+
return result("unknown", "Control flow is outside the bounded handler classifier.");
|
|
67
|
+
}
|
|
68
|
+
function blockOutcome(statements, checker, contracts) {
|
|
69
|
+
for (const statement of statements) {
|
|
70
|
+
const outcome = statementOutcome(statement, checker, contracts);
|
|
71
|
+
if (outcome)
|
|
72
|
+
return outcome;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function containsReturn(node) {
|
|
76
|
+
if (ts.isReturnStatement(node))
|
|
77
|
+
return true;
|
|
78
|
+
if (ts.isFunctionLike(node))
|
|
79
|
+
return false;
|
|
80
|
+
return ts.forEachChild(node, containsReturn) ?? false;
|
|
81
|
+
}
|
|
82
|
+
function guardedFallback(statement, next, checker, contracts) {
|
|
83
|
+
if (!next || !ts.isIfStatement(next) || statement.tryBlock.statements.length !== 1)
|
|
84
|
+
return false;
|
|
85
|
+
const assignment = statement.tryBlock.statements[0];
|
|
86
|
+
if (!ts.isExpressionStatement(assignment) || !ts.isBinaryExpression(assignment.expression) ||
|
|
87
|
+
assignment.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assignment.expression.left))
|
|
88
|
+
return false;
|
|
89
|
+
const symbol = checker.getSymbolAtLocation(assignment.expression.left);
|
|
90
|
+
const declarations = symbol?.declarations ?? [];
|
|
91
|
+
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || declarations[0].initializer)
|
|
92
|
+
return false;
|
|
93
|
+
const refersToBinding = (node) => (ts.isIdentifier(node) && checker.getSymbolAtLocation(node) === symbol) ||
|
|
94
|
+
(ts.forEachChild(node, refersToBinding) ?? false);
|
|
95
|
+
if (refersToBinding(assignment.expression.right))
|
|
96
|
+
return false;
|
|
97
|
+
// Require the uninitialized binding immediately before the try: no earlier assigned state.
|
|
98
|
+
const parent = statement.parent;
|
|
99
|
+
if (!ts.isBlock(parent) && !ts.isSourceFile(parent))
|
|
100
|
+
return false;
|
|
101
|
+
const previous = parent.statements[parent.statements.indexOf(statement) - 1];
|
|
102
|
+
if (!previous || !ts.isVariableStatement(previous) || previous.declarationList.declarations.length !== 1 ||
|
|
103
|
+
!(previous.declarationList.flags & ts.NodeFlags.Let) || previous.declarationList.declarations[0] !== declarations[0])
|
|
104
|
+
return false;
|
|
105
|
+
const condition = unwrapHandler(next.expression);
|
|
106
|
+
if (!ts.isBinaryExpression(condition) || condition.operatorToken.kind !== ts.SyntaxKind.EqualsEqualsEqualsToken)
|
|
107
|
+
return false;
|
|
108
|
+
const left = unwrapHandler(condition.left), right = unwrapHandler(condition.right);
|
|
109
|
+
if (!ts.isIdentifier(left) || checker.getSymbolAtLocation(left) !== symbol || !ts.isIdentifier(right) || right.text !== "undefined")
|
|
110
|
+
return false;
|
|
111
|
+
const undefinedDeclarations = checker.getSymbolAtLocation(right)?.declarations ?? [];
|
|
112
|
+
if (undefinedDeclarations.some(declaration => !declaration.getSourceFile().hasNoDefaultLib))
|
|
113
|
+
return false;
|
|
114
|
+
const outcome = statementOutcome(next.thenStatement, checker, contracts);
|
|
115
|
+
return !!outcome && ["failureResult", "propagated", "reported"].includes(outcome.classification);
|
|
116
|
+
}
|
|
117
|
+
export function classifyDisposition(body, checker, empty, comments, use, contracts) {
|
|
118
|
+
const owner = body.parent;
|
|
119
|
+
// Finally may replace a throw/return, even when the catch itself is explicit.
|
|
120
|
+
if (ts.isCatchClause(owner)) {
|
|
121
|
+
for (let ancestor = owner.parent; ancestor && !ts.isFunctionLike(ancestor); ancestor = ancestor.parent)
|
|
122
|
+
if (ts.isTryStatement(ancestor) && ancestor.finallyBlock)
|
|
123
|
+
return result("unknown", "Finally may change the failure disposition.");
|
|
124
|
+
}
|
|
125
|
+
const intent = [...(empty ? comments : comments.filter(comment => intentional.test(comment))), ...declaredIntent(body, checker)];
|
|
126
|
+
if (intent.length)
|
|
127
|
+
return { ...result("documented", "Attached developer intent is honored without judging the business decision."), comments: intent };
|
|
128
|
+
if (!empty) {
|
|
129
|
+
const outcome = ts.isBlock(body) ? blockOutcome(body.statements, checker, contracts) : explicitValue(body) || contracts.explicitResult(body) ? result("failureResult", "Explicit value/container from rejection handler.") : undefined;
|
|
130
|
+
return outcome ?? result("unknown", "Code presence alone does not establish a failure disposition.");
|
|
131
|
+
}
|
|
132
|
+
if (!ts.isCatchClause(owner)) {
|
|
133
|
+
if (!ts.isExpressionStatement(use.parent))
|
|
134
|
+
return result("unknown", "The rejection handler's result is used by an enclosing expression or caller; its failure contract needs context.");
|
|
135
|
+
return result("unexplainedSwallowing", "Empty rejection handler resolves without a value or declared intent; its result is discarded.");
|
|
136
|
+
}
|
|
137
|
+
const statement = owner.parent, parent = statement.parent;
|
|
138
|
+
if (!ts.isBlock(parent) && !ts.isSourceFile(parent))
|
|
139
|
+
return result("unknown", "Enclosing control flow requires context.");
|
|
140
|
+
const following = parent.statements.slice(parent.statements.indexOf(statement) + 1);
|
|
141
|
+
if (guardedFallback(statement, following[0], checker, contracts))
|
|
142
|
+
return result("fallback", "Failed assignment is followed by an explicit undefined guard and failure outcome.");
|
|
143
|
+
if (following.length) {
|
|
144
|
+
const outcome = blockOutcome(following, checker, contracts);
|
|
145
|
+
if (outcome && outcome.classification !== "unknown")
|
|
146
|
+
return result("fallback", outcome.reason);
|
|
147
|
+
return result("unknown", "Continuation after the empty catch needs a failure-contract review.");
|
|
148
|
+
}
|
|
149
|
+
if (containsReturn(statement.tryBlock) && contracts.guardedUndefinedResult(body))
|
|
150
|
+
return result("fallback", "Every selected-source call tests the optional result before using it; no escaping function reference was found.");
|
|
151
|
+
if (containsReturn(statement.tryBlock))
|
|
152
|
+
return result("unknown", "Success returns a value; the implicit/undefined failure result may be a caller contract.");
|
|
153
|
+
if (!ts.isSourceFile(parent) && !ts.isFunctionLike(parent.parent))
|
|
154
|
+
return result("unknown", "Outer continuation or control flow requires context.");
|
|
155
|
+
return result("unexplainedSwallowing", "Terminal empty catch discards failure without an explicit result or attached intent.");
|
|
156
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { defaultEvidencePath, defaultMetricsPath, dimensionKeys, ecosystemId, metricsCsvHeader, type DimensionKey, } from "./scorecard-contract.js";
|
|
2
2
|
export { analyze } from "./analyzer.js";
|
|
3
|
+
export { enrichDependencies } from "./dependency-evidence.js";
|
|
4
|
+
export type { RegistryReader } from "./dependency-metadata.js";
|
|
3
5
|
export { compare, gate, sarif, validateEvidence, readEvidence } from "./evidence-tools.js";
|
|
4
6
|
export type { Evidence, Finding, Dimension } from "./evidence.js";
|
|
5
7
|
export { readCompatibleEvidence, inspectEvidence, type LegacyEvidence } from "./evidence-reader.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { defaultEvidencePath, defaultMetricsPath, dimensionKeys, ecosystemId, metricsCsvHeader, } from "./scorecard-contract.js";
|
|
2
2
|
export { analyze } from "./analyzer.js";
|
|
3
|
+
export { enrichDependencies } from "./dependency-evidence.js";
|
|
3
4
|
export { compare, gate, sarif, validateEvidence, readEvidence } from "./evidence-tools.js";
|
|
4
5
|
export { readCompatibleEvidence, inspectEvidence } from "./evidence-reader.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python callback adapter context
|
|
2
|
+
|
|
3
|
+
`async_adapters.py` implements `python/async-callback-adapters-review-v1` as an experimental report embedded at `dimensions.performanceAsync.adapterReview` in development adapter 0.14.0, ruleset `python-2026-10-24-async-adapters-review-v1`. It supplements the [async context report](ASYNC_CONTEXT.md) without changing its original observations. The archive ships this guide and the producer; the companion skill reports the evidence without runtime claims. Historical 0.13.0 reports retain their contract without invented adapter observations. **Async/Blocking Usage and Error Handling remain unscored.** Method Complexity remains Python's only scored dimension.
|
|
4
|
+
|
|
5
|
+
```powershell
|
|
6
|
+
py -3.12 analyzers/python/async_adapters.py E:/repos/example --output E:/temp/adapters.json
|
|
7
|
+
py -3.12 analyzers/python/async_adapters_corpus.py --repos E:/repos --output TestResults/python-adapters-new
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The same source-selection flags and optional run/audit UUIDs as the original async reviewer are supported. Output must be a new `.json` file with an existing parent; the corpus directory must be new. Target code is parsed, never imported or executed.
|
|
11
|
+
|
|
12
|
+
## Partial construction
|
|
13
|
+
|
|
14
|
+
Import-matched `functools.partial` calls record the construction extent, first positional callback expression, and bound positional/keyword expression shapes and extents. Literal argument values are not copied. `*args` and `**kwargs` are explicit syntax, not known argument counts. With a missing or starred first positional argument, callback layout is unresolved; the tool does not interpret `func=` as a callback. Callback references use the existing conservative local-definition/import-spelling rules. Calls, lambdas, receiver methods, and nested partial results are not resolved to callback implementations.
|
|
15
|
+
|
|
16
|
+
These are construction observations, not proof that a partial was created, callable, executed, or offloaded successfully. Eager callback/argument calls retain their original owner. Callback and lambda bodies are never traversed to assign behavior to the async caller.
|
|
17
|
+
|
|
18
|
+
An inline partial passed to a recognized helper or used as a callee links to its original owned construction call ID. A stored partial supports a direct-suite, single-name assignment in the same async owner, ending before use, with exactly one binding and a first positional callback. Reassignment, parameters, aliases, conditional/annotated/chained assignments, deletion, reflective operations, cross-scope writes, and comprehension shadowing withhold that link. Stored links in deferred generator bodies are withheld. Inline constructions in deferred bodies remain separately labeled syntax.
|
|
19
|
+
|
|
20
|
+
For example, Starlette's `func = functools.partial(func, *args, **kwargs)` is a visible construction, but the parameter and assignment make `func` ambiguous. The subsequent helper callback remains unresolved. We do not infer the incoming callable's identity.
|
|
21
|
+
|
|
22
|
+
Supported stored `sync_to_async` wrappers also expose partial callback context evaluated at construction. Repeated callback references link to the same construction. Construction, callback references, and invocation expressions must never be added together as distinct offloaded operations.
|
|
23
|
+
|
|
24
|
+
## Decorator links
|
|
25
|
+
|
|
26
|
+
A bare-name callee or helper callback can link to a unique direct lexical `def` with exactly one import-matched `asgiref.sync.sync_to_async` decorator. Supported syntax is `@sync_to_async`, `@sync_to_async()`, or a factory with one literal boolean `thread_sensitive` keyword. Import aliases are supported. The decorator is resolved in the definition's enclosing scope, not the caller's scope.
|
|
27
|
+
|
|
28
|
+
The definition must end before the reference, have a selected source-fact owner, and have no competing binding. Stacked or unknown decorators, unsupported arguments, starred arguments, async definitions, conditional definitions, late definitions, receiver methods, and ambiguous/mutated imports are unresolved. Class namespaces are not lexical closures for methods. A positive link records a synchronous source definition and decorator syntax; it does not reclassify that owner's body as async or validate passing a decorated callable to a thread helper. Decorator headers are not added to the original async call population.
|
|
29
|
+
|
|
30
|
+
## Accounting and validation
|
|
31
|
+
|
|
32
|
+
Every original async call keeps its ID and direct/deferred execution context. Construction and reference counts are separate, as are production, test, support, and generated populations. Sync-only and empty populations are unavailable, with null measurements. Zero matches are not a clean async verdict. Existing unresolved import counts are unchanged.
|
|
33
|
+
|
|
34
|
+
The report carries source selection, source-facts producer/policy, original async-row hash, configuration fingerprint, and matching run/audit IDs. It regenerates the input report before collection and verifies source hashes and exact owner/call measurements while collecting. Stale, inconsistent, changed, over-budget, or failed inputs produce exit 2, diagnostics, empty rows, and a null summary. These checks are not report authentication, filesystem locking, control-flow proof, or runtime identity analysis.
|
|
35
|
+
|
|
36
|
+
No score, finding, deduction, gate, automatic concurrency recommendation, or judgment of developer intent is emitted.
|
|
37
|
+
|
|
38
|
+
The package validator checks internal consistency against validated async/context reports: invocation and source metadata, original-row hash, ordered owners/calls, extents, construction links and repeated references, selected local definitions, decorator options, and recomputed context/population summaries. It is not independent AST remeasurement or report authentication. Incomplete required evidence makes the combined scorecard unusable; observations cannot restore a score. See the [installed-package study](../../shared/scorecard-schema/calibration-runs/python-async-adapters-package-2026-10-24.md).
|