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,157 @@
1
+ """Deterministic, overridable selection. These categories are scope, not defects."""
2
+
3
+ import ast
4
+ from fnmatch import fnmatchcase
5
+ import hashlib
6
+ import io
7
+ import json
8
+ from pathlib import PurePosixPath
9
+ import re
10
+ import tokenize
11
+
12
+
13
+ POLICY = "python/source-selection-v1"
14
+ GENERATED = re.compile(r"^\s*#\s*(?:generated by\b|this file (?:is|was) (?:automatically )?generated\b|automatically generated\b|code generated\b)", re.I)
15
+
16
+
17
+ def matches(file, patterns):
18
+ for pattern in patterns:
19
+ if fnmatchcase(file, pattern):
20
+ return True
21
+ while pattern.startswith("**/"):
22
+ pattern = pattern[3:]
23
+ if fnmatchcase(file, pattern):
24
+ return True
25
+ return False
26
+
27
+
28
+ def selection_options(include=(), exclude=(), test_paths=(), production_paths=(), include_tests=False, include_generated=False, include_support=False):
29
+ options = {
30
+ "include": sorted(set(include)), "exclude": sorted(set(exclude)),
31
+ "testPaths": sorted(set(test_paths)), "productionPaths": sorted(set(production_paths)),
32
+ "includeTests": include_tests, "includeGenerated": include_generated, "includeSupport": include_support,
33
+ }
34
+ for key in ("include", "exclude", "testPaths", "productionPaths"):
35
+ for pattern in options[key]:
36
+ if not pattern or "\\" in pattern or pattern.startswith("/") or ":" in pattern or ".." in pattern.split("/"):
37
+ raise ValueError("Selection patterns must be nonempty root-relative POSIX patterns without '..'.")
38
+ return options
39
+
40
+
41
+ def inventory(options):
42
+ fingerprint = hashlib.sha256(json.dumps({"policy": POLICY, "options": options}, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
43
+ return {"policy": POLICY, "configurationFingerprint": fingerprint, "options": options,
44
+ "files": [], "prunedDirectories": [], "excludedFunctions": []}
45
+
46
+
47
+ def category(file, options):
48
+ path = PurePosixPath(file)
49
+ if path.suffix == ".pyi":
50
+ return "declaration", "stub-file"
51
+ if matches(file, options["productionPaths"]):
52
+ return "production", "explicit-production-path"
53
+ if matches(file, options["testPaths"]):
54
+ return "test", "explicit-test-path"
55
+ if any(part in {"test", "tests"} for part in path.parts[:-1]) or path.name == "conftest.py" or path.name.startswith("test_") or path.name.endswith("_test.py"):
56
+ return "test", "test-path-convention"
57
+ if any(part in {"docs", "doc", "examples", "benchmarks"} for part in path.parts[:-1]) or file in {"setup.py", "noxfile.py"}:
58
+ return "support", "support-path-convention"
59
+ return "production", "production-candidate"
60
+
61
+
62
+ def file_record(file, options):
63
+ kind, reason = category(file, options)
64
+ disposition = "analyzed"
65
+ if options["include"] and not matches(file, options["include"]):
66
+ disposition, reason = "excluded", "outside-include-selection"
67
+ elif matches(file, options["exclude"]):
68
+ disposition, reason = "excluded", "explicit-exclusion"
69
+ elif kind == "declaration" or (kind == "test" and not options["includeTests"]) or (kind == "support" and not options["includeSupport"]):
70
+ disposition = "excluded"
71
+ return {"file": file, "category": kind, "disposition": disposition, "reason": reason, "contentSha256": None}
72
+
73
+
74
+ def generated_source(source):
75
+ try:
76
+ for token in tokenize.generate_tokens(io.StringIO(source).readline):
77
+ if token.start[0] > 20:
78
+ break
79
+ if token.type == tokenize.COMMENT and GENERATED.match(token.string):
80
+ return True
81
+ except (tokenize.TokenError, IndentationError):
82
+ pass # The AST parser will report malformed selected source.
83
+ return False
84
+
85
+
86
+ def bound_names(node):
87
+ """Conservative whole-file shadow check; uncertainty keeps implementations in scope."""
88
+ names = set()
89
+ for child in ast.walk(node):
90
+ if isinstance(child, ast.Name) and isinstance(child.ctx, (ast.Store, ast.Del)):
91
+ names.add(child.id)
92
+ elif isinstance(child, ast.arg):
93
+ names.add(child.arg)
94
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
95
+ names.add(child.name)
96
+ elif isinstance(child, ast.ExceptHandler) and child.name:
97
+ names.add(child.name)
98
+ elif isinstance(child, (ast.MatchAs, ast.MatchStar)) and child.name:
99
+ names.add(child.name)
100
+ elif isinstance(child, ast.MatchMapping) and child.rest:
101
+ names.add(child.rest)
102
+ elif isinstance(child, (ast.Attribute, ast.Subscript)) and isinstance(child.ctx, (ast.Store, ast.Del)):
103
+ base = child.value
104
+ while isinstance(base, (ast.Attribute, ast.Subscript)):
105
+ base = base.value
106
+ if isinstance(base, ast.Name):
107
+ names.add(base.id)
108
+ return names
109
+
110
+
111
+ def import_bindings(tree):
112
+ bindings, seen = {}, {}
113
+ top_level = {id(node) for node in tree.body}
114
+ for node in ast.walk(tree):
115
+ pairs = []
116
+ if isinstance(node, ast.Import):
117
+ pairs = [(item.asname or item.name.split(".")[0], item.name if item.asname else item.name.split(".")[0]) for item in node.names]
118
+ elif isinstance(node, ast.ImportFrom):
119
+ pairs = [(item.asname or item.name, f"{node.module}.{item.name}" if not node.level else "<relative>") for item in node.names]
120
+ for name, target in pairs:
121
+ seen.setdefault(name, set()).add(target)
122
+ if id(node) in top_level:
123
+ bindings[name] = target
124
+ blocked = bound_names(tree)
125
+ if "*" in seen:
126
+ return {}
127
+ return {name: target for name, target in bindings.items() if name not in blocked and seen[name] == {target}}
128
+
129
+
130
+ def resolve(node, bindings):
131
+ if isinstance(node, ast.Subscript):
132
+ return resolve(node.value, bindings)
133
+ if isinstance(node, ast.Name):
134
+ return bindings.get(node.id)
135
+ if isinstance(node, ast.Attribute):
136
+ owner = resolve(node.value, bindings)
137
+ return f"{owner}.{node.attr}" if owner else None
138
+ return None
139
+
140
+
141
+ def declaration_reason(node, bindings, protocol_method=False):
142
+ if isinstance(node, ast.Lambda):
143
+ return None
144
+ body = node.body
145
+ if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str):
146
+ body = body[1:]
147
+ placeholder = all(isinstance(item, ast.Pass) or (isinstance(item, ast.Expr) and isinstance(item.value, ast.Constant) and item.value.value is Ellipsis) for item in body)
148
+ if not placeholder:
149
+ return None
150
+ decorators = {resolve(item, bindings) for item in node.decorator_list}
151
+ if decorators & {"typing.overload", "typing_extensions.overload"}:
152
+ return "overload-declaration"
153
+ if "abc.abstractmethod" in decorators:
154
+ return "abstract-declaration"
155
+ if protocol_method:
156
+ return "protocol-declaration"
157
+ return None
@@ -0,0 +1,21 @@
1
+ export interface PythonOptions {
2
+ project: string;
3
+ python?: string;
4
+ runId?: string;
5
+ auditId?: string;
6
+ include?: string[];
7
+ exclude?: string[];
8
+ testPath?: string[];
9
+ productionPath?: string[];
10
+ includeTests?: boolean;
11
+ includeGenerated?: boolean;
12
+ includeSupport?: boolean;
13
+ coverageJson?: string;
14
+ collectionManifest?: string;
15
+ }
16
+ export declare function pythonTarget(project: string): {
17
+ entryPoint: string;
18
+ root: string;
19
+ target: string;
20
+ };
21
+ export declare function analyzePython(options: PythonOptions): import("./evidence.js").Evidence;
@@ -0,0 +1,123 @@
1
+ import { validateAsyncHelpers, validateAsyncScoring } from "./python-async-scoring.js";
2
+ import { validateAsyncAdapters } from "./python-async-adapters.js";
3
+ import { validateAsyncContext } from "./python-async-context.js";
4
+ import { validateAsyncUsage } from "./python-async-usage.js";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { spawnSync } from "node:child_process";
9
+ import { validateSourceFacts } from "#source-facts/contract.mjs";
10
+ import { invocationIds } from "./evidence.js";
11
+ import { pythonEvidence } from "./python-evidence.js";
12
+ import { validateDuplication } from "./python-duplication.js";
13
+ import { validateCoverage } from "./python-coverage.js";
14
+ import { validateOperations } from "./python-error-operations.js";
15
+ import { validateTransfers } from "./python-error-transfers.js";
16
+ import { validateErrorContext } from "./python-error-context.js";
17
+ import { validateErrorHandling } from "./python-error-handling.js";
18
+ import { validateDecomposition } from "./python-decomposition.js";
19
+ export function pythonTarget(project) {
20
+ const requested = path.resolve(project);
21
+ if (fs.lstatSync(requested).isSymbolicLink())
22
+ throw new Error("Linked Python entry points are not supported.");
23
+ // Python's collection receipts expand Windows short names with Path.resolve().
24
+ // Normalize at collection/analysis entry, never by consulting disk when reading historical evidence.
25
+ const entryPoint = fs.realpathSync.native(requested);
26
+ const directory = fs.statSync(entryPoint).isDirectory();
27
+ const manifest = path.basename(entryPoint) === "pyproject.toml";
28
+ if (!directory && !manifest && !/\.pyi?$/.test(entryPoint))
29
+ throw new Error("Python entry point must be a directory, pyproject.toml, .py, or .pyi file.");
30
+ const root = directory ? entryPoint : path.dirname(entryPoint);
31
+ return { entryPoint, root, target: manifest ? root : entryPoint };
32
+ }
33
+ export function analyzePython(options) {
34
+ const ids = invocationIds(options.runId, options.auditId);
35
+ const { target, root, entryPoint } = pythonTarget(options.project);
36
+ const script = fileURLToPath(new URL("./python/analysis.py", import.meta.url));
37
+ // Isolated stdlib interpreter; only the packaged analyzer directory is added to sys.path.
38
+ const bootstrap = "import runpy,sys; from pathlib import Path; script=sys.argv[1]; sys.path.insert(0,str(Path(script).parent)); sys.argv=sys.argv[1:]; runpy.run_path(script,run_name='__main__')";
39
+ const command = options.python ?? (process.platform === "win32" ? "py" : "python3");
40
+ if (options.python && !path.isAbsolute(options.python))
41
+ throw new Error("--python must be an absolute interpreter path.");
42
+ const args = [...(!options.python && process.platform === "win32" ? ["-3.12"] : []), "-I", "-S", "-B", "-c", bootstrap, script, target];
43
+ for (const [flag, items] of [["include", options.include], ["exclude", options.exclude], ["test-path", options.testPath], ["production-path", options.productionPath]])
44
+ for (const item of items ?? [])
45
+ args.push(`--${flag}`, item);
46
+ for (const [flag, enabled] of [["include-tests", options.includeTests], ["include-generated", options.includeGenerated], ["include-support", options.includeSupport]])
47
+ if (enabled)
48
+ args.push(`--${flag}`);
49
+ let asyncHelpers = null;
50
+ let asyncScoring = null;
51
+ let asyncAdapters = null;
52
+ let asyncContext = null;
53
+ let asyncReview = null;
54
+ let operations = null;
55
+ let transfers = null;
56
+ let context = null;
57
+ let errorHandling = null;
58
+ let facts = null;
59
+ let duplication = null;
60
+ let decomposition = null;
61
+ let coverage = null;
62
+ for (const [flag, value] of [["coverage-json", options.coverageJson], ["collection-manifest", options.collectionManifest]])
63
+ if (value)
64
+ args.push(`--${flag}`, path.resolve(value));
65
+ const subject = { root, entryPoint, name: path.basename(root), variant: "source" };
66
+ args.push("--run-id", ids.runId, "--audit-id", ids.auditId);
67
+ let error;
68
+ const result = spawnSync(command, args, { cwd: path.dirname(script), encoding: "utf8", windowsHide: true, timeout: 1_800_000, maxBuffer: 128 * 1024 * 1024 });
69
+ try {
70
+ if (result.error)
71
+ throw new Error(`Python 3.12 could not run: ${result.error.message}`);
72
+ if (result.status !== 0 && result.status !== 2)
73
+ throw new Error(`Python exited ${result.status}: ${result.stderr.slice(0, 2000)}`);
74
+ if (!result.stdout.trim())
75
+ throw new Error(`Python emitted no facts (exit ${result.status}): ${result.stderr.slice(0, 2000)}`);
76
+ const parsed = JSON.parse(result.stdout);
77
+ validateSourceFacts(parsed.sourceFacts);
78
+ validateDuplication(parsed.duplication, parsed.sourceFacts, subject, ids);
79
+ validateCoverage(parsed.coverageReview, parsed.sourceFacts, subject, ids);
80
+ validateDecomposition(parsed.decomposition, parsed.sourceFacts, subject, ids);
81
+ validateErrorHandling(parsed.errorHandlingReview, parsed.sourceFacts, subject, ids);
82
+ validateErrorContext(parsed.errorHandlingContext, parsed.errorHandlingReview);
83
+ validateTransfers(parsed.errorHandlingTransfers, parsed.errorHandlingContext);
84
+ validateOperations(parsed.errorHandlingOperations, parsed.errorHandlingTransfers);
85
+ validateAsyncUsage(parsed.asyncUsageReview, parsed.sourceFacts, subject, ids, parsed.decomposition);
86
+ validateAsyncContext(parsed.asyncContextReview, parsed.asyncUsageReview);
87
+ validateAsyncAdapters(parsed.asyncAdapterReview, parsed.asyncUsageReview, parsed.asyncContextReview);
88
+ validateAsyncHelpers(parsed.asyncHelperReview, parsed.asyncUsageReview, parsed.asyncContextReview, parsed.asyncAdapterReview);
89
+ validateAsyncScoring(parsed.asyncScoring, parsed.asyncHelperReview, parsed.asyncUsageReview);
90
+ asyncHelpers = parsed.asyncHelperReview;
91
+ asyncScoring = parsed.asyncScoring;
92
+ asyncAdapters = parsed.asyncAdapterReview;
93
+ asyncContext = parsed.asyncContextReview;
94
+ asyncReview = parsed.asyncUsageReview;
95
+ operations = parsed.errorHandlingOperations;
96
+ transfers = parsed.errorHandlingTransfers;
97
+ context = parsed.errorHandlingContext;
98
+ errorHandling = parsed.errorHandlingReview;
99
+ decomposition = parsed.decomposition;
100
+ facts = parsed.sourceFacts;
101
+ duplication = parsed.duplication;
102
+ coverage = parsed.coverageReview;
103
+ if ((result.status === 0) !== (facts.status === "complete" && duplication.status === "complete" && coverage.status === "complete" && decomposition.status === "complete" && errorHandling.status === "complete" && context.status === "complete" && transfers.status === "complete" && operations.status === "complete" && asyncReview.status === "complete" && asyncContext.status === "complete" && asyncAdapters.status === "complete" && asyncHelpers.status === "complete" && asyncScoring.status === "complete"))
104
+ throw new Error("Python exit status and facts disagree.");
105
+ }
106
+ catch (failure) {
107
+ asyncHelpers = null;
108
+ asyncScoring = null;
109
+ asyncAdapters = null;
110
+ asyncContext = null;
111
+ asyncReview = null;
112
+ operations = null;
113
+ transfers = null;
114
+ context = null;
115
+ errorHandling = null;
116
+ facts = null;
117
+ decomposition = null;
118
+ duplication = null;
119
+ coverage = null;
120
+ error = failure instanceof Error ? failure.message : String(failure);
121
+ }
122
+ return pythonEvidence(facts, subject, ids, error, duplication, coverage, decomposition, errorHandling, context, transfers, operations, asyncReview, asyncContext, asyncAdapters, asyncHelpers, asyncScoring);
123
+ }
@@ -0,0 +1,67 @@
1
+ import { type Extent, type Reference, type PythonAsyncContext } from "./python-async-context.js";
2
+ import type { PythonAsyncUsage } from "./python-async-usage.js";
3
+ export declare const asyncAdaptersPolicy = "python/async-callback-adapters-review-v1";
4
+ interface Expression extends Extent {
5
+ kind: string;
6
+ shape: string;
7
+ }
8
+ interface Partial {
9
+ status: string;
10
+ importTarget: string;
11
+ construction: Extent;
12
+ argumentLayout: string;
13
+ callback: (Expression & {
14
+ reference: Reference;
15
+ }) | null;
16
+ boundPositionalExpressions: Expression[] | null;
17
+ keywordExpressions: {
18
+ name: string | null;
19
+ unpacking: boolean;
20
+ value: Expression;
21
+ }[];
22
+ }
23
+ interface PartialLink {
24
+ status: string;
25
+ reason?: string;
26
+ definition?: null;
27
+ name?: string;
28
+ assignment?: Extent;
29
+ constructionCallId?: string;
30
+ partial?: Partial;
31
+ }
32
+ interface DecoratorLink {
33
+ status: string;
34
+ reason?: string;
35
+ importTarget?: string;
36
+ definition: (Extent & {
37
+ ownerId: string;
38
+ file: string;
39
+ member: string;
40
+ functionKind: string;
41
+ }) | null;
42
+ decorator?: Extent & {
43
+ form: string;
44
+ options: {
45
+ name: string;
46
+ value: boolean;
47
+ }[];
48
+ };
49
+ }
50
+ interface AdapterCall {
51
+ callId: string;
52
+ executionContext: string;
53
+ partialConstruction: Partial | null;
54
+ partialCallee: PartialLink;
55
+ decoratedCallee: DecoratorLink;
56
+ callbackPartial: PartialLink | null;
57
+ callbackDecorator: DecoratorLink | null;
58
+ }
59
+ interface AdapterOwner extends Omit<PythonAsyncContext["functions"][number], "calls"> {
60
+ calls: AdapterCall[];
61
+ }
62
+ export interface PythonAsyncAdapters extends Omit<PythonAsyncContext, "functions"> {
63
+ functions: AdapterOwner[];
64
+ }
65
+ /** Internal consistency checks against validated source evidence, not AST remeasurement or authentication. */
66
+ export declare function validateAsyncAdapters(value: unknown, base: PythonAsyncUsage, context: PythonAsyncContext): asserts value is PythonAsyncAdapters;
67
+ export {};
@@ -0,0 +1,197 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ import { fingerprint } from "./python-error-context.js";
4
+ import { before, contains, extent, keys, reference } from "./python-async-context.js";
5
+ export const asyncAdaptersPolicy = "python/async-callback-adapters-review-v1";
6
+ const locationKeys = ["line", "column", "endLine", "endColumn"];
7
+ const partialReasons = ["not-a-name-or-matched-partial", "comprehension-local-binding", "reflective-or-cross-scope-binding",
8
+ "deferred-execution-context", "not-a-unique-same-owner-binding", "no-direct-assignment-before-use", "not-a-matched-partial-assignment", "unresolved-argument-layout"];
9
+ const decoratorReasons = ["non-name-reference", "comprehension-local-binding", "reflective-or-cross-scope-binding",
10
+ "rebound-or-ambiguous-name", "no-direct-definition-before-reference", "unsupported-decorated-definition",
11
+ "unmatched-decorator-import", "unsupported-decorator-arguments", "definition-outside-selected-owners", "no-local-definition"];
12
+ const nonempty = (v) => typeof v === "string" && v.trim().length > 0;
13
+ const path = (v) => typeof v === "string" && /^[\p{ID_Start}_][\p{ID_Continue}]*(\.[\p{ID_Start}_][\p{ID_Continue}]*)*$/u.test(v);
14
+ function check(condition, message) {
15
+ if (!condition)
16
+ throw new Error(`Invalid Python async adapter evidence: ${message}.`);
17
+ }
18
+ function location(value) { keys(value, locationKeys); extent(value); }
19
+ function sameExtent(a, b) { return locationKeys.every(key => a[key] === b[key]); }
20
+ function expression(value, container, extra = []) {
21
+ keys(value, [...locationKeys, "kind", "shape", ...extra]);
22
+ extent(value);
23
+ check(contains(container, value), "expression outside construction");
24
+ const kinds = ["Name", "Attribute", "Call", "Subscript", "Lambda", "Constant", "Starred", "BoolOp", "NamedExpr", "BinOp", "UnaryOp", "IfExp", "Dict", "Set", "ListComp", "SetComp", "DictComp", "GeneratorExp", "Await", "Yield", "YieldFrom", "Compare", "FormattedValue", "JoinedStr", "List", "Tuple"];
25
+ check(kinds.includes(value.kind), "unknown expression kind");
26
+ const expected = { Name: ["name"], Attribute: ["attribute-path", "attribute-on-expression"], Call: ["call-result"], Subscript: ["subscript"], Lambda: ["lambda"] }[value.kind] ?? ["other-expression"];
27
+ check(expected.includes(value.shape), "expression kind/shape mismatch");
28
+ }
29
+ function construction(value, call, owner, owners) {
30
+ keys(value, ["status", "importTarget", "construction", "argumentLayout", "callback", "boundPositionalExpressions", "keywordExpressions"]);
31
+ check(value.status === "partial-construction-syntax" && value.importTarget === "functools.partial" && call.binding.target === value.importTarget, "unmatched partial constructor");
32
+ location(value.construction);
33
+ check(sameExtent(value.construction, call), "construction extent mismatch");
34
+ check(Array.isArray(value.keywordExpressions), "keyword collection required");
35
+ if (value.argumentLayout === "unresolved") {
36
+ check(value.callback === null && value.boundPositionalExpressions === null, "unresolved argument layout must withhold callback");
37
+ }
38
+ else {
39
+ check(value.argumentLayout === "first-positional-callback" && value.callback && value.callback.kind !== "Starred" && Array.isArray(value.boundPositionalExpressions), "invalid callback layout");
40
+ const callback = value.callback;
41
+ expression(callback, call, ["reference"]);
42
+ const r = callback.reference;
43
+ if (r.status === "import-spelling")
44
+ check(path(r.target) && ["Name", "Attribute"].includes(callback.kind), "invalid callback import spelling");
45
+ reference(r, r.status === "local-definition" ? r.definition?.member.split(".").at(-1) ?? null : null, r.status === "import-spelling" ? r.target : null, callback.kind, { ...call, ...callback }, owner, owners, "");
46
+ let previous = callback;
47
+ for (const arg of value.boundPositionalExpressions) {
48
+ expression(arg, call);
49
+ check(before(previous, arg), "positional arguments out of order");
50
+ previous = arg;
51
+ }
52
+ }
53
+ let previousKeyword = null;
54
+ for (const kw of value.keywordExpressions) {
55
+ keys(kw, ["name", "unpacking", "value"]);
56
+ check(kw.name === null || path(kw.name) && !kw.name.includes("."), "invalid keyword name");
57
+ check(kw.unpacking === (kw.name === null), "keyword unpacking mismatch");
58
+ expression(kw.value, call);
59
+ check(!previousKeyword || before(previousKeyword, kw.value), "keywords out of order");
60
+ previousKeyword = kw.value;
61
+ }
62
+ }
63
+ function unresolvedLink(value, reasons) {
64
+ keys(value, ["status", "reason", "definition"]);
65
+ check(value.status === "unresolved" && value.definition === null && reasons.includes(value.reason), "invalid unresolved link");
66
+ }
67
+ function partialLink(value, use, owner, calls, rows, kind, spelling, bindingReason) {
68
+ if (kind === "Name" && ["comprehension-local-binding", "reflective-or-cross-scope-binding"].includes(bindingReason))
69
+ check(value.status === "unresolved" && value.reason === bindingReason, "partial binding veto lost");
70
+ if (value.status === "unresolved") {
71
+ unresolvedLink(value, partialReasons);
72
+ return;
73
+ }
74
+ const stored = value.status === "stored-partial-syntax";
75
+ keys(value, stored ? ["status", "name", "assignment", "constructionCallId", "partial"] : ["status", "constructionCallId", "partial"]);
76
+ check(stored || value.status === "inline-partial-syntax", "unsupported partial link");
77
+ const call = calls.get(value.constructionCallId), original = rows.get(value.constructionCallId)?.partialConstruction;
78
+ check(call && original && isDeepStrictEqual(value.partial, original) && value.constructionCallId === fingerprint([owner.id, original.construction]), "missing or inconsistent construction link");
79
+ if (stored) {
80
+ check(kind === "Name" && value.name === spelling && use.executionContext === "async-body" && call.executionContext === "async-body" && !call.directlyAwaited && original.argumentLayout === "first-positional-callback", "invalid stored partial context");
81
+ location(value.assignment);
82
+ check(value.assignment.line >= owner.line && value.assignment.endLine <= owner.endLine && contains(value.assignment, call) && before(value.assignment, use), "invalid partial assignment/source order");
83
+ }
84
+ else
85
+ check(kind === "Call" && call.id !== use.id && contains(use, call) && use.executionContext === call.executionContext, "invalid inline partial context");
86
+ }
87
+ function decoratorLink(value, use, owner, owners, kind, spelling, bindingReason, old) {
88
+ if (kind === "Name" && ["comprehension-local-binding", "reflective-or-cross-scope-binding"].includes(bindingReason))
89
+ check(value.status === "unresolved" && value.reason === bindingReason, "decorator binding veto lost");
90
+ if (value.status === "unresolved") {
91
+ unresolvedLink(value, decoratorReasons);
92
+ return;
93
+ }
94
+ keys(value, ["status", "importTarget", "definition", "decorator"]);
95
+ check(value.status === "decorated-wrapper-syntax" && value.importTarget === "asgiref.sync.sync_to_async" && kind === "Name" &&
96
+ old?.status === "unresolved" && old.reason === "decorated-definition", "unsupported decorated reference");
97
+ const d = value.definition;
98
+ keys(d, [...locationKeys, "ownerId", "file", "member", "functionKind"]);
99
+ extent(d);
100
+ const selected = owners.get(d.ownerId);
101
+ check(selected && selected.functionKind === "FunctionDef" && d.functionKind === "FunctionDef" && d.file === owner.file && d.file === selected.file && d.member === selected.member &&
102
+ d.line === selected.line && d.endLine === selected.endLine && spelling === d.member.split(".").at(-1) &&
103
+ d.ownerId === hash(`${d.file}|${d.line}|${d.column}|FunctionDef`) && before(d, use), "decorated definition mismatch");
104
+ const decorator = value.decorator;
105
+ keys(decorator, [...locationKeys, "form", "options"]);
106
+ extent(decorator);
107
+ check(before(decorator, d) && ["bare", "factory"].includes(decorator.form) && Array.isArray(decorator.options) &&
108
+ decorator.options.length <= (decorator.form === "bare" ? 0 : 1), "invalid decorator header");
109
+ for (const option of decorator.options) {
110
+ keys(option, ["name", "value"]);
111
+ check(option.name === "thread_sensitive" && typeof option.value === "boolean", "unsupported decorator option");
112
+ }
113
+ }
114
+ function validateCall(row, call, owner, owners, calls, rows, old) {
115
+ keys(row, ["callId", "executionContext", "partialConstruction", "partialCallee", "decoratedCallee", "callbackPartial", "callbackDecorator"]);
116
+ check(row.callId === call.id && row.executionContext === call.executionContext, "call identity/context mismatch");
117
+ if (call.binding.target === "functools.partial") {
118
+ check(row.partialConstruction, "missing partial construction");
119
+ construction(row.partialConstruction, call, owner, owners);
120
+ }
121
+ else
122
+ check(row.partialConstruction === null, "unexpected partial construction");
123
+ const kind = old.shape === "name" ? "Name" : old.shape === "call-result" ? "Call" : "Other";
124
+ partialLink(row.partialCallee, call, owner, calls, rows, kind, call.binding.syntax, call.binding.reason);
125
+ decoratorLink(row.decoratedCallee, call, owner, owners, kind, call.binding.syntax, call.binding.reason, old.localCallee);
126
+ let use = call;
127
+ if (old.storedWrapper?.status === "stored-wrapper-syntax")
128
+ use = calls.get(old.storedWrapper.definition.constructionCallId);
129
+ const callback = use.callback;
130
+ if (!old.callback) {
131
+ check(row.callbackPartial === null && row.callbackDecorator === null, "unexpected callback adapter");
132
+ return;
133
+ }
134
+ check(callback?.binding && row.callbackPartial && row.callbackDecorator, "missing callback adapter");
135
+ partialLink(row.callbackPartial, use, owner, calls, rows, callback.kind, callback.binding.syntax, callback.binding.reason);
136
+ decoratorLink(row.callbackDecorator, use, owner, owners, callback.kind, callback.binding.syntax, callback.binding.reason, old.callback.reference);
137
+ if (use !== call) {
138
+ check(isDeepStrictEqual(row.callbackPartial, rows.get(use.id)?.callbackPartial) && isDeepStrictEqual(row.callbackDecorator, rows.get(use.id)?.callbackDecorator), "stored wrapper callback differs from construction");
139
+ }
140
+ if (call.category === "offload-wrapped-invocation") {
141
+ const inner = [...calls.values()].find(c => c.category === "offload-wrapper-construction" && c.line === call.line && c.column === call.column && contains(call, c) && c.id !== call.id);
142
+ check(inner && isDeepStrictEqual(row.callbackPartial, rows.get(inner.id)?.callbackPartial) && isDeepStrictEqual(row.callbackDecorator, rows.get(inner.id)?.callbackDecorator), "immediate wrapper callback differs from construction");
143
+ }
144
+ }
145
+ function counts(rows) {
146
+ const eligible = rows.filter(row => row.status === "observed"), calls = eligible.flatMap(row => row.calls);
147
+ return { selectedFunctions: rows.length, asyncFunctions: eligible.length, excludedSynchronousFunctions: rows.length - eligible.length,
148
+ availability: eligible.length ? "available" : "unavailable", contexts: Object.fromEntries(["async-body", "deferred-generator-body"].map(context => {
149
+ const selected = calls.filter(call => call.executionContext === context);
150
+ const links = (field, statuses) => selected.filter(call => call[field] && statuses.includes(call[field].status)).length;
151
+ return [context, eligible.length ? { callExpressions: selected.length,
152
+ partialConstructions: links("partialConstruction", ["partial-construction-syntax"]),
153
+ partialCalleeReferences: links("partialCallee", ["inline-partial-syntax", "stored-partial-syntax"]),
154
+ partialCallbackReferences: links("callbackPartial", ["inline-partial-syntax", "stored-partial-syntax"]),
155
+ decoratedCalleeReferences: links("decoratedCallee", ["decorated-wrapper-syntax"]),
156
+ decoratedCallbackReferences: links("callbackDecorator", ["decorated-wrapper-syntax"]) } : null];
157
+ })) };
158
+ }
159
+ /** Internal consistency checks against validated source evidence, not AST remeasurement or authentication. */
160
+ export function validateAsyncAdapters(value, base, context) {
161
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "subject", "selection", "sourceFactsProducer", "sourceFactsPolicy", "asyncRowsSha256", "status", "diagnostics", "functions", "summary", "scope"]);
162
+ const r = value;
163
+ check(r.schemaVersion === 1 && r.artifactKind === "experimental-python-async-callback-adapters-review" && r.policy === asyncAdaptersPolicy &&
164
+ isDeepStrictEqual(r.producer, { name: "codemetrics-ai/python-async-callback-adapters", version: "0.1.0-experimental" }), "unsupported producer/policy");
165
+ for (const key of ["runId", "auditId", "subject", "selection", "sourceFactsProducer", "sourceFactsPolicy"])
166
+ check(isDeepStrictEqual(r[key], base[key]), "invocation/source mismatch");
167
+ const configuration = { policy: asyncAdaptersPolicy, asyncConfigurationFingerprint: base.configurationFingerprint };
168
+ check(isDeepStrictEqual(r.configuration, configuration) && r.configurationFingerprint === fingerprint(configuration) && r.asyncRowsSha256 === fingerprint(base.functions), "configuration/original rows mismatch");
169
+ check(nonempty(r.scope) && typeof r.generatedAtUtc === "string" && Number.isFinite(Date.parse(r.generatedAtUtc)), "scope/time required");
170
+ check(Array.isArray(r.functions) && Array.isArray(r.diagnostics), "collections required");
171
+ for (const d of r.diagnostics) {
172
+ keys(d, ["code", "message"]);
173
+ check(d.code === "async-adapter-analysis" && nonempty(d.message), "invalid diagnostic");
174
+ }
175
+ if (r.status === "incomplete") {
176
+ check(r.diagnostics.length > 0 && r.functions.length === 0 && r.summary === null, "incomplete report must withhold rows");
177
+ return;
178
+ }
179
+ check(r.status === "complete" && base.status === "complete" && !r.diagnostics.length && r.functions.length === base.functions.length, "status/owner population mismatch");
180
+ // A context-only failure can coexist with valid adapter syntax, but cannot restore a score.
181
+ const owners = new Map(base.functions.map(row => [row.id, row]));
182
+ for (let i = 0; i < r.functions.length; i++) {
183
+ const row = r.functions[i], owner = base.functions[i];
184
+ keys(row, ["id", "file", "member", "line", "endLine", "population", "sourceSha256", "status", "reason", "disposition", "calls"]);
185
+ for (const key of ["id", "file", "member", "line", "endLine", "population", "sourceSha256", "status", "reason", "disposition"])
186
+ check(row[key] === owner[key], "owner metadata/order mismatch");
187
+ const original = owner.measurement?.calls ?? [];
188
+ check(Array.isArray(row.calls) && row.calls.length === original.length, "call population mismatch");
189
+ const calls = new Map(original.map(call => [call.id, call])), rows = new Map(row.calls.map(call => [call.callId, call]));
190
+ check(context.status === "complete" || !row.calls.length, "complete adapter calls require validated context");
191
+ for (let j = 0; j < row.calls.length; j++)
192
+ validateCall(row.calls[j], original[j], owner, owners, calls, rows, context.functions[i].calls[j]);
193
+ }
194
+ const expected = { ...counts(r.functions), populations: Object.fromEntries([...new Set(r.functions.map(row => row.population))].sort()
195
+ .map(population => [population, counts(r.functions.filter(row => row.population === population))])) };
196
+ check(isDeepStrictEqual(r.summary, expected), "summary mismatch");
197
+ }
@@ -0,0 +1,73 @@
1
+ import type { PythonAsyncUsage } from "./python-async-usage.js";
2
+ export declare const asyncContextPolicy = "python/async-call-context-review-v1";
3
+ type Owner = PythonAsyncUsage["functions"][number];
4
+ type Call = NonNullable<Owner["measurement"]>["calls"][number];
5
+ export interface Extent {
6
+ line: number;
7
+ column: number;
8
+ endLine: number;
9
+ endColumn: number;
10
+ }
11
+ interface Definition extends Extent {
12
+ ownerId: string;
13
+ file: string;
14
+ member: string;
15
+ functionKind: string;
16
+ }
17
+ interface WrapperDefinition {
18
+ name: string;
19
+ assignment: Extent;
20
+ constructionCallId: string;
21
+ construction: Extent;
22
+ importTarget: string;
23
+ }
24
+ export interface Reference {
25
+ status: string;
26
+ reason: string;
27
+ target?: string;
28
+ definition: Definition | null;
29
+ }
30
+ interface Wrapper {
31
+ status: string;
32
+ reason: string;
33
+ definition: WrapperDefinition | null;
34
+ }
35
+ interface Callback {
36
+ kind: string;
37
+ evaluation: string;
38
+ reference: Reference;
39
+ }
40
+ interface ContextCall {
41
+ callId: string;
42
+ executionContext: string;
43
+ shape: string;
44
+ unresolvedReason: string | null;
45
+ localCallee: Reference | null;
46
+ storedWrapper: Wrapper | null;
47
+ callback: Callback | null;
48
+ }
49
+ interface ContextOwner {
50
+ id: string;
51
+ file: string;
52
+ member: string;
53
+ line: number;
54
+ endLine: number;
55
+ population: string;
56
+ sourceSha256: string;
57
+ status: string;
58
+ reason: string | null;
59
+ disposition: string;
60
+ calls: ContextCall[];
61
+ }
62
+ export interface PythonAsyncContext extends Omit<PythonAsyncUsage, "functions"> {
63
+ asyncRowsSha256: string;
64
+ functions: ContextOwner[];
65
+ }
66
+ export declare function keys(value: unknown, names: string[]): void;
67
+ export declare const before: (a: Extent, b: Extent) => boolean;
68
+ export declare const contains: (a: Extent, b: Extent) => boolean;
69
+ export declare function extent(value: Extent): void;
70
+ export declare function reference(value: Reference, spelling: string | null, target: string | null, kind: string, use: Call, owner: Owner, owners: Map<string, Owner>, bindingReason: string): void;
71
+ /** Check consistency against validated async evidence; not independent AST resolution or authentication. */
72
+ export declare function validateAsyncContext(value: unknown, base: PythonAsyncUsage): asserts value is PythonAsyncContext;
73
+ export {};