codemetrics-ai 0.5.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 (98) hide show
  1. package/README.md +31 -1
  2. package/dist/analyzer.d.ts +1 -0
  3. package/dist/analyzer.js +8 -4
  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/evidence-reader.js +3 -1
  16. package/dist/evidence-tools.js +4 -0
  17. package/dist/function-scoring.d.ts +2 -1
  18. package/dist/function-scoring.js +13 -24
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/python/ASYNC_ADAPTERS.md +38 -0
  22. package/dist/python/ASYNC_CONTEXT.md +58 -0
  23. package/dist/python/ASYNC_SCORING.md +174 -0
  24. package/dist/python/ASYNC_USAGE.md +62 -0
  25. package/dist/python/BEHAVIOR_REVIEW.md +84 -0
  26. package/dist/python/COVERAGE.md +96 -0
  27. package/dist/python/DECOMPOSITION.md +46 -0
  28. package/dist/python/DUPLICATION.md +36 -0
  29. package/dist/python/ERROR_HANDLING.md +45 -0
  30. package/dist/python/ERROR_HANDLING_CONTEXT.md +61 -0
  31. package/dist/python/ERROR_HANDLING_OPERATIONS.md +54 -0
  32. package/dist/python/ERROR_HANDLING_TRANSFERS.md +64 -0
  33. package/dist/python/METHOD_COMPLEXITY.md +46 -0
  34. package/dist/python/README.md +162 -0
  35. package/dist/python/analysis.py +54 -0
  36. package/dist/python/async_adapters.py +243 -0
  37. package/dist/python/async_bindings.py +180 -0
  38. package/dist/python/async_context.py +143 -0
  39. package/dist/python/async_helper_paths.py +186 -0
  40. package/dist/python/async_provenance.py +129 -0
  41. package/dist/python/async_scoring.py +203 -0
  42. package/dist/python/async_usage.py +244 -0
  43. package/dist/python/coverage_collect.py +121 -0
  44. package/dist/python/coverage_provenance.py +77 -0
  45. package/dist/python/coverage_review.py +273 -0
  46. package/dist/python/coverage_worker.py +46 -0
  47. package/dist/python/decomposition.py +199 -0
  48. package/dist/python/duplication.py +177 -0
  49. package/dist/python/error_handling.py +249 -0
  50. package/dist/python/error_handling_context.py +294 -0
  51. package/dist/python/error_handling_operations.py +228 -0
  52. package/dist/python/error_handling_transfers.py +211 -0
  53. package/dist/python/review_cli.py +36 -0
  54. package/dist/python/source_facts.py +293 -0
  55. package/dist/python/source_selection.py +157 -0
  56. package/dist/python-analyzer.d.ts +21 -0
  57. package/dist/python-analyzer.js +123 -0
  58. package/dist/python-async-adapters.d.ts +67 -0
  59. package/dist/python-async-adapters.js +197 -0
  60. package/dist/python-async-context.d.ts +73 -0
  61. package/dist/python-async-context.js +181 -0
  62. package/dist/python-async-scoring.d.ts +89 -0
  63. package/dist/python-async-scoring.js +249 -0
  64. package/dist/python-async-usage.d.ts +56 -0
  65. package/dist/python-async-usage.js +143 -0
  66. package/dist/python-cli.d.ts +2 -0
  67. package/dist/python-cli.js +53 -0
  68. package/dist/python-collect-cli.d.ts +2 -0
  69. package/dist/python-collect-cli.js +28 -0
  70. package/dist/python-complexity.d.ts +18 -0
  71. package/dist/python-complexity.js +36 -0
  72. package/dist/python-coverage.d.ts +63 -0
  73. package/dist/python-coverage.js +173 -0
  74. package/dist/python-decomposition.d.ts +56 -0
  75. package/dist/python-decomposition.js +102 -0
  76. package/dist/python-duplication.d.ts +65 -0
  77. package/dist/python-duplication.js +89 -0
  78. package/dist/python-error-context.d.ts +84 -0
  79. package/dist/python-error-context.js +165 -0
  80. package/dist/python-error-handling.d.ts +56 -0
  81. package/dist/python-error-handling.js +120 -0
  82. package/dist/python-error-operations.d.ts +44 -0
  83. package/dist/python-error-operations.js +155 -0
  84. package/dist/python-error-transfers.d.ts +38 -0
  85. package/dist/python-error-transfers.js +125 -0
  86. package/dist/python-evidence.d.ts +23 -0
  87. package/dist/python-evidence.js +255 -0
  88. package/dist/score-arithmetic.d.ts +1 -12
  89. package/dist/score-arithmetic.js +1 -40
  90. package/dist/shared-source-facts/arithmetic.d.mts +9 -0
  91. package/dist/shared-source-facts/arithmetic.mjs +32 -0
  92. package/dist/shared-source-facts/contract.d.mts +35 -0
  93. package/dist/shared-source-facts/contract.mjs +95 -0
  94. package/dist/shared-source-facts/population.d.mts +12 -0
  95. package/dist/shared-source-facts/population.mjs +41 -0
  96. package/dist/source-facts.d.ts +4 -0
  97. package/dist/source-facts.js +16 -0
  98. package/package.json +15 -6
@@ -0,0 +1,120 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ export const errorHandlingPolicy = "python/exception-handler-observations-v1";
4
+ const documentationPolicy = "suite-comments-or-string-statements/tool-directives-excluded/content-not-adjudicated-v1";
5
+ const classifications = ["documented-no-op", "undocumented-no-op", "terminal-raise", "terminal-return", "terminal-loop-control", "other-body"];
6
+ function requireValue(condition, message) {
7
+ if (!condition)
8
+ throw new Error(`Invalid Python error-handling evidence: ${message}.`);
9
+ }
10
+ function keys(value, expected) {
11
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
12
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), expected.sort()), "unexpected or missing fields");
13
+ }
14
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
15
+ const nonempty = (value) => typeof value === "string" && value.trim().length > 0;
16
+ function histogram(values) {
17
+ return Object.fromEntries([...new Set(values)].sort().map(value => [value, values.filter(item => item === value).length]));
18
+ }
19
+ function summary(rows) {
20
+ const counts = (items) => ({ handlers: items.length, byClassification: histogram(items.map(item => item.classification)) });
21
+ return { ...counts(rows), functionsWithHandlers: new Set(rows.map(row => row.owner.functionId).filter(Boolean)).size,
22
+ byOwnerKind: histogram(rows.map(row => row.owner.kind)),
23
+ populations: Object.fromEntries([...new Set(rows.map(row => row.population))].sort().map(kind => [kind, counts(rows.filter(row => row.population === kind))])) };
24
+ }
25
+ function annotations(value, row, directives) {
26
+ requireValue(Array.isArray(value), "annotations required");
27
+ for (const item of value) {
28
+ keys(item, ["kind", "line", "text"]);
29
+ requireValue((directives ? ["comment"] : ["comment", "string-statement"]).includes(item.kind) &&
30
+ integer(item.line) && item.line >= row.line && nonempty(item.text) && /[\p{L}\p{N}]/u.test(item.text), "invalid annotation");
31
+ // Trailing suite comments can extend beyond the AST handler's endLine.
32
+ const directive = /^(?:noqa\b|type:\s*ignore\b|pragma:\s*no cover\b|fmt:|pylint:|ruff:)/i.test(item.text);
33
+ requireValue(item.kind !== "comment" || directive === directives, "directive/documentation mismatch");
34
+ requireValue(item.kind !== "string-statement" || item.line <= row.endLine, "string outside handler");
35
+ }
36
+ }
37
+ function body(row) {
38
+ const value = row.bodyObservations;
39
+ keys(value, ["raises", "returns", "loopTransfers", "calls", "nestedHandlersExcluded"]);
40
+ requireValue(integer(value.calls) && integer(value.nestedHandlersExcluded), "invalid owned counts");
41
+ for (const [kind, fields] of [["raises", ["line", "bare", "explicitCause"]], ["returns", ["line", "hasValue"]], ["loopTransfers", ["line", "kind"]]]) {
42
+ requireValue(Array.isArray(value[kind]), "owned transfers required");
43
+ for (const item of value[kind]) {
44
+ keys(item, [...fields]);
45
+ requireValue(integer(item.line) && item.line >= row.line && item.line <= row.endLine, "transfer outside handler");
46
+ if ("bare" in item)
47
+ requireValue(typeof item.bare === "boolean" && typeof item.explicitCause === "boolean", "invalid raise");
48
+ if ("hasValue" in item)
49
+ requireValue(typeof item.hasValue === "boolean", "invalid return");
50
+ if ("kind" in item)
51
+ requireValue(["break", "continue"].includes(item.kind), "invalid loop transfer");
52
+ }
53
+ }
54
+ const inert = row.classification.endsWith("no-op");
55
+ if (inert)
56
+ requireValue(!value.raises.length && !value.returns.length && !value.loopTransfers.length && !value.calls && !value.nestedHandlersExcluded, "no-op has active syntax");
57
+ else
58
+ requireValue(!row.documentation.length && !row.directiveComments.length, "non-inert annotations outside policy");
59
+ requireValue(row.classification !== "documented-no-op" || row.documentation.length > 0, "documented no-op needs annotation");
60
+ requireValue(row.classification !== "undocumented-no-op" || !row.documentation.length, "undocumented no-op has annotation");
61
+ for (const [kind, count] of [["terminal-raise", value.raises.length], ["terminal-return", value.returns.length], ["terminal-loop-control", value.loopTransfers.length]])
62
+ requireValue(row.classification !== kind || Number(count) > 0, "terminal classification lacks transfer");
63
+ }
64
+ /** Checks provenance and internal consistency, not independent AST remeasurement or authentication. */
65
+ export function validateErrorHandling(value, facts, subject, ids) {
66
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "status", "policy", "configuration",
67
+ "configurationFingerprint", "subject", "sourceFactsProducer", "sourceFactsPolicy", "selection", "diagnostics", "scope", "handlers", "summary"]);
68
+ const report = value;
69
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-error-handling-review" && report.policy === errorHandlingPolicy, "unsupported policy");
70
+ requireValue(isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-error-handling", version: "0.1.0-experimental" }), "producer mismatch");
71
+ requireValue(report.runId === ids.runId && report.auditId === ids.auditId, "stale invocation identities");
72
+ const target = subject.entryPoint.replaceAll("\\", "/").endsWith("/pyproject.toml") ? subject.root : subject.entryPoint;
73
+ requireValue(isDeepStrictEqual(report.subject, { root: subject.root, entryPoint: target }), "subject mismatch");
74
+ requireValue(isDeepStrictEqual(report.sourceFactsProducer, facts.producer) && report.sourceFactsPolicy === facts.measurementPolicy &&
75
+ isDeepStrictEqual(report.selection, facts.selection), "source selection/provenance mismatch");
76
+ const configuration = { policy: errorHandlingPolicy, selectionFingerprint: facts.selection.configurationFingerprint,
77
+ parserVersion: facts.producer.parserVersion, maxHandlers: 100000, documentationPolicy };
78
+ requireValue(isDeepStrictEqual(report.configuration, configuration), "configuration mismatch");
79
+ requireValue(report.configurationFingerprint === hash(JSON.stringify(configuration, Object.keys(configuration).sort())), "configuration fingerprint mismatch");
80
+ requireValue(nonempty(report.scope) && typeof report.generatedAtUtc === "string" && Number.isFinite(Date.parse(report.generatedAtUtc)), "missing scope or generation time");
81
+ requireValue(Array.isArray(report.handlers) && Array.isArray(report.diagnostics), "collections required");
82
+ for (const diagnostic of report.diagnostics) {
83
+ keys(diagnostic, diagnostic.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
84
+ requireValue(nonempty(diagnostic.code) && nonempty(diagnostic.message) && (diagnostic.file === undefined || nonempty(diagnostic.file)), "invalid diagnostic");
85
+ }
86
+ if (report.status === "incomplete") {
87
+ requireValue(report.diagnostics.length > 0 && report.handlers.length === 0 && report.summary === null, "incomplete results must withhold measurements");
88
+ return;
89
+ }
90
+ requireValue(report.status === "complete" && facts.status === "complete" && !report.diagnostics.length && report.handlers.length <= 100000, "status or budget mismatch");
91
+ const owners = new Map(facts.functions.map(item => [item.id, item]));
92
+ const files = new Map(facts.selection.files.map(item => [item.file, item]));
93
+ const used = new Set();
94
+ for (const row of report.handlers) {
95
+ keys(row, ["id", "file", "line", "endLine", "sourceSha256", "population", "owner", "exceptionTypeSyntax", "bareExcept", "exceptionGroup",
96
+ "enclosingFinallyLines", "classification", "documentation", "directiveComments", "bodyObservations", "disposition"]);
97
+ const file = files.get(row.file);
98
+ requireValue(typeof row.id === "string" && /^[a-f0-9]{64}$/.test(row.id) && !used.has(row.id), "invalid or repeated handler identity");
99
+ used.add(row.id);
100
+ requireValue(file?.disposition === "analyzed" && row.sourceSha256 === file.contentSha256 && row.population === file.category &&
101
+ integer(row.line) && row.line > 0 && integer(row.endLine) && row.endLine >= row.line && row.disposition === "review-only", "location/source mismatch");
102
+ keys(row.owner, ["kind", "member", "functionId"]);
103
+ requireValue(nonempty(row.owner.member), "owner member required");
104
+ if (row.owner.kind === "function") {
105
+ const owner = owners.get(row.owner.functionId);
106
+ requireValue(owner && owner.file === row.file && owner.member === row.owner.member && owner.population === row.population && row.line >= owner.line, "function owner mismatch");
107
+ }
108
+ else
109
+ requireValue(["module", "class-body"].includes(row.owner.kind) && row.owner.functionId === null &&
110
+ (row.owner.kind !== "module" || row.owner.member === "<module>"), "invalid nonfunction owner");
111
+ requireValue(typeof row.bareExcept === "boolean" && typeof row.exceptionGroup === "boolean" &&
112
+ (row.bareExcept ? row.exceptionTypeSyntax === null && !row.exceptionGroup : nonempty(row.exceptionTypeSyntax)), "exception syntax mismatch");
113
+ requireValue(Array.isArray(row.enclosingFinallyLines) && row.enclosingFinallyLines.every((line, index, lines) => integer(line) && line > 0 && line < row.line && (index === 0 || line > lines[index - 1])), "invalid finally context");
114
+ requireValue(classifications.includes(row.classification), "unknown classification");
115
+ annotations(row.documentation, row, false);
116
+ annotations(row.directiveComments, row, true);
117
+ body(row);
118
+ }
119
+ requireValue(isDeepStrictEqual(report.summary, summary(report.handlers)), "summary accounting mismatch");
120
+ }
@@ -0,0 +1,44 @@
1
+ import { type PythonErrorContext } from "./python-error-context.js";
2
+ import { type Expression, type PythonTransfers } from "./python-error-transfers.js";
3
+ type Transfer = PythonTransfers["handlers"][number];
4
+ interface Location {
5
+ id: string;
6
+ line: number;
7
+ column: number;
8
+ endLine: number;
9
+ endColumn: number;
10
+ ancestors: {
11
+ kind: string;
12
+ line: number;
13
+ }[];
14
+ ancestorsTruncated: boolean;
15
+ containsNamedExpression: boolean;
16
+ }
17
+ interface Call extends Location {
18
+ callee: Expression;
19
+ loggingNameSyntax: string | null;
20
+ directStatement: boolean;
21
+ immediatelyAwaited: boolean;
22
+ positionalArguments: number;
23
+ keywordArguments: number;
24
+ starredArguments: number;
25
+ keywordUnpacks: number;
26
+ }
27
+ interface Return extends Location {
28
+ direct: boolean;
29
+ expression: Expression;
30
+ awaitedExpression: Expression | null;
31
+ }
32
+ interface Handler extends Pick<Transfer, "handlerId" | "file" | "line" | "sourceSha256" | "population" | "owner" | "originalClassification" | "classification" | "disposition"> {
33
+ calls: Call[];
34
+ returns: Return[];
35
+ suite: PythonErrorContext["contexts"][number]["handlerBody"];
36
+ }
37
+ export interface PythonOperations extends Omit<PythonTransfers, "contextReview" | "contextRowsSha256" | "handlers"> {
38
+ transferReview: PythonTransfers;
39
+ transferRowsSha256: string;
40
+ handlers: Handler[];
41
+ }
42
+ /** Base must already be validated. Checks consistency, not independent AST measurement or authenticity. */
43
+ export declare function validateOperations(value: unknown, base: PythonTransfers): asserts value is PythonOperations;
44
+ export {};
@@ -0,0 +1,155 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ import { fingerprint } from "./python-error-context.js";
4
+ import { expression } from "./python-error-transfers.js";
5
+ const policy = "python/exception-call-return-v1";
6
+ const loggingNames = ["critical", "debug", "error", "exception", "fatal", "info", "log", "warn", "warning"];
7
+ function requireValue(value, message) {
8
+ if (!value)
9
+ throw new Error(`Invalid Python call/return evidence: ${message}.`);
10
+ }
11
+ function keys(value, names) {
12
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
13
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), names.sort()), "unexpected or missing fields");
14
+ }
15
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
16
+ const nonempty = (value) => typeof value === "string" && value.trim().length > 0;
17
+ const histogram = (values) => Object.fromEntries([...new Set(values)].sort().map(key => [key, values.filter(value => value === key).length]));
18
+ function inventory(rows) {
19
+ const items = rows.filter(row => row.originalClassification.status === "unresolved");
20
+ const shapes = new Map();
21
+ for (const row of items) {
22
+ const kinds = row.suite.statements.map(item => item.kind), truncated = row.suite.truncated;
23
+ const key = JSON.stringify([kinds, truncated]);
24
+ const entry = shapes.get(key) ?? { kinds, truncated, handlers: 0 };
25
+ entry.handlers++;
26
+ shapes.set(key, entry);
27
+ }
28
+ const compareKinds = (left, right) => {
29
+ for (let index = 0; index < Math.min(left.length, right.length); index++)
30
+ if (left[index] !== right[index])
31
+ return left[index] < right[index] ? -1 : 1;
32
+ return left.length - right.length;
33
+ };
34
+ return { handlers: items.length, singleStatement: items.filter(row => row.suite.statementCount === 1).length,
35
+ multiStatement: items.filter(row => row.suite.statementCount > 1).length, withCalls: items.filter(row => row.calls.length).length,
36
+ withReturns: items.filter(row => row.returns.length).length, withLoggingNameSyntax: items.filter(row => row.calls.some(call => call.loggingNameSyntax)).length,
37
+ byReason: histogram(items.flatMap(row => row.originalClassification.unresolvedReasons)),
38
+ suiteShapes: [...shapes.values()].sort((a, b) => b.handlers - a.handlers || compareKinds(a.kinds, b.kinds) || Number(a.truncated) - Number(b.truncated)) };
39
+ }
40
+ function summary(rows) {
41
+ const counts = (items) => {
42
+ const calls = items.flatMap(row => row.calls), returns = items.flatMap(row => row.returns);
43
+ return { handlers: items.length, recognizedShapes: items.filter(row => row.classification.status === "recognized-shape").length,
44
+ unresolved: items.filter(row => row.classification.status === "unresolved").length,
45
+ byPattern: histogram(items.flatMap(row => row.classification.pattern ? [row.classification.pattern] : [])),
46
+ unresolvedReasons: histogram(items.flatMap(row => row.classification.unresolvedReasons)), callExpressions: calls.length, returnStatements: returns.length,
47
+ handlersWithCalls: items.filter(row => row.calls.length).length, handlersWithReturns: items.filter(row => row.returns.length).length,
48
+ loggingNameSyntaxCalls: calls.filter(row => row.loggingNameSyntax !== null).length, immediatelyAwaitedCalls: calls.filter(row => row.immediatelyAwaited).length,
49
+ byReturnExpression: histogram(returns.map(row => row.expression.kind)),
50
+ newlyRecognizedShapes: items.filter(row => row.originalClassification.status === "unresolved" && row.classification.status === "recognized-shape").length,
51
+ inputUnresolvedInventory: inventory(items) };
52
+ };
53
+ return { ...counts(rows), populations: Object.fromEntries([...new Set(rows.map(row => row.population))].sort().map(kind => [kind, counts(rows.filter(row => row.population === kind))])) };
54
+ }
55
+ function classification(row, prior) {
56
+ if (prior.classification.status === "recognized-shape" || !isDeepStrictEqual(prior.classification.unresolvedReasons, ["no-supported-local-shape"]) ||
57
+ row.calls.some(call => call.containsNamedExpression) || row.returns.some(item => item.containsNamedExpression))
58
+ return prior.classification;
59
+ let pattern = null;
60
+ let lines = [];
61
+ const returned = row.returns.find(item => item.direct);
62
+ if (row.suite.statementCount === 1 && returned && ["name", "attribute", "call"].includes(returned.expression.kind) && returned.expression.symbol !== null) {
63
+ pattern = "direct-symbol-or-call-return";
64
+ lines = [returned.line];
65
+ }
66
+ else {
67
+ const calls = row.calls.filter(call => call.directStatement);
68
+ const raises = prior.raises.filter(item => item.direct);
69
+ const rethrow = raises.length === 1 && raises[0].exception.kind === "absent" && row.suite.statementCount > 1;
70
+ if (calls.length > 0 && calls.length + Number(rethrow) === row.suite.statementCount && !returned &&
71
+ calls.every(call => !call.immediatelyAwaited && ["name", "attribute"].includes(call.callee.kind) && call.callee.symbol !== null) &&
72
+ (!rethrow || calls.every(call => call.line < raises[0].line || call.line === raises[0].line && call.column < raises[0].column))) {
73
+ pattern = rethrow ? "direct-calls-then-bare-raise" : "direct-call-statement-sequence";
74
+ lines = calls.map(call => call.ancestors[0].line).concat(rethrow ? [raises[0].line] : []);
75
+ }
76
+ }
77
+ return pattern ? { status: "recognized-shape", pattern, evidenceLines: [...new Set([row.line, ...lines])].sort((a, b) => a - b), unresolvedReasons: [] } : prior.classification;
78
+ }
79
+ const locationKeys = ["id", "line", "column", "endLine", "endColumn", "ancestors", "ancestorsTruncated", "containsNamedExpression"];
80
+ function location(item, previous, row, endLine, kind, seen) {
81
+ requireValue(integer(item.line) && item.line >= row.line && integer(item.column) && integer(item.endLine) && item.endLine >= item.line && item.endLine <= endLine && integer(item.endColumn) && (item.endLine > item.line || item.endColumn > item.column) &&
82
+ (!previous || item.line > previous.line || item.line === previous.line && item.column > previous.column || item.line === previous.line && item.column === previous.column && (item.endLine < previous.endLine || item.endLine === previous.endLine && item.endColumn < previous.endColumn)), "invalid location/order");
83
+ requireValue(item.id === hash(`${row.file}|${item.line}|${item.column}|${item.endLine}|${item.endColumn}|${kind}`) && !seen.has(item.id), "operation identity mismatch");
84
+ seen.add(item.id);
85
+ requireValue(typeof item.containsNamedExpression === "boolean" && typeof item.ancestorsTruncated === "boolean" && Array.isArray(item.ancestors) &&
86
+ item.ancestors.length <= 32 && (!item.ancestorsTruncated || item.ancestors.length === 32), "invalid ancestor accounting");
87
+ for (const [index, ancestor] of item.ancestors.entries()) {
88
+ keys(ancestor, ["kind", "line"]);
89
+ requireValue(typeof ancestor.kind === "string" && /^[A-Z][A-Za-z]*$/.test(ancestor.kind) && integer(ancestor.line) && ancestor.line >= row.line &&
90
+ ancestor.line <= item.line && (!index || ancestor.line <= item.ancestors[index - 1].line), "invalid ancestor metadata");
91
+ }
92
+ }
93
+ /** Base must already be validated. Checks consistency, not independent AST measurement or authenticity. */
94
+ export function validateOperations(value, base) {
95
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "transferReview", "transferRowsSha256", "status", "diagnostics", "handlers", "summary", "scope"]);
96
+ const report = value;
97
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-exception-operation-review" && report.policy === policy &&
98
+ isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-exception-operations", version: "0.2.0-experimental" }), "unsupported producer/policy");
99
+ requireValue(report.runId === base.runId && report.auditId === base.auditId && isDeepStrictEqual(report.transferReview, base) && report.transferRowsSha256 === fingerprint(base.handlers), "transfer provenance mismatch");
100
+ const configuration = { policy, transferPolicy: base.policy, transferConfigurationFingerprint: base.configurationFingerprint,
101
+ maxOperations: 100000, maxAncestors: 32, maxSuiteStatements: 32, maxSymbolCharacters: 200, maxSymbolAttributes: 8, loggingNames };
102
+ requireValue(isDeepStrictEqual(report.configuration, configuration) && report.configurationFingerprint === fingerprint(configuration), "configuration mismatch");
103
+ requireValue(nonempty(report.scope) && typeof report.generatedAtUtc === "string" && Number.isFinite(Date.parse(report.generatedAtUtc)) && Array.isArray(report.handlers) && Array.isArray(report.diagnostics), "invalid metadata/collections");
104
+ for (const item of report.diagnostics) {
105
+ keys(item, item.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
106
+ requireValue(nonempty(item.code) && nonempty(item.message) && (item.file === undefined || nonempty(item.file)), "invalid diagnostic");
107
+ }
108
+ if (report.status === "incomplete") {
109
+ requireValue(report.diagnostics.length > 0 && !report.handlers.length && report.summary === null, "incomplete report must withhold rows");
110
+ return;
111
+ }
112
+ requireValue(report.status === "complete" && base.status === "complete" && !report.diagnostics.length && report.handlers.length === base.handlers.length, "status/population mismatch");
113
+ const prior = new Map(base.handlers.map(row => [row.handlerId, row]));
114
+ const contexts = new Map(base.contextReview.contexts.map(row => [row.handlerId, row]));
115
+ const originals = new Map(base.contextReview.handlerReview.handlers.map(row => [row.id, row]));
116
+ const used = new Set(), seen = new Set();
117
+ for (const row of report.handlers) {
118
+ keys(row, ["handlerId", "file", "line", "sourceSha256", "population", "owner", "calls", "returns", "suite", "originalClassification", "classification", "disposition"]);
119
+ const old = prior.get(row.handlerId), context = contexts.get(row.handlerId), original = originals.get(row.handlerId);
120
+ requireValue(old && context && original && !used.has(row.handlerId), "missing or repeated handler");
121
+ used.add(row.handlerId);
122
+ requireValue(["file", "line", "sourceSha256", "population", "owner"].every(key => isDeepStrictEqual(row[key], old[key])) &&
123
+ row.disposition === "review-only" && isDeepStrictEqual(row.originalClassification, old.classification) && isDeepStrictEqual(row.suite, context.handlerBody) &&
124
+ Array.isArray(row.calls) && Array.isArray(row.returns), "handler provenance or suite mismatch");
125
+ for (const [index, call] of row.calls.entries()) {
126
+ keys(call, [...locationKeys, "callee", "loggingNameSyntax", "directStatement", "immediatelyAwaited", "positionalArguments", "keywordArguments", "starredArguments", "keywordUnpacks"]);
127
+ location(call, row.calls[index - 1], row, original.endLine, "Call", seen);
128
+ expression(call.callee, old.handlerNameSyntax);
129
+ const terminal = ["name", "attribute"].includes(call.callee.kind) ? call.callee.symbol?.split(".").at(-1) : undefined;
130
+ requireValue(call.loggingNameSyntax === (terminal && loggingNames.includes(terminal) ? terminal : null), "logging-name hint mismatch");
131
+ requireValue(call.immediatelyAwaited === (call.ancestors[0]?.kind === "Await") && call.directStatement === (!call.ancestorsTruncated &&
132
+ (call.ancestors.length === 1 && call.ancestors[0].kind === "Expr" || call.ancestors.length === 2 && call.ancestors[0].kind === "Await" && call.ancestors[1].kind === "Expr")), "call context mismatch");
133
+ requireValue([call.positionalArguments, call.keywordArguments, call.starredArguments, call.keywordUnpacks].every(integer) &&
134
+ call.starredArguments <= call.positionalArguments && call.keywordUnpacks <= call.keywordArguments, "invalid argument accounting");
135
+ if (call.directStatement && !row.suite.truncated)
136
+ requireValue(row.suite.statements.some(item => item.kind === "Expr" && item.line === call.ancestors.at(-1).line && item.endLine >= call.endLine), "direct call absent from suite");
137
+ }
138
+ for (const [index, item] of row.returns.entries()) {
139
+ keys(item, [...locationKeys, "direct", "expression", "awaitedExpression"]);
140
+ location(item, row.returns[index - 1], row, original.endLine, "Return", seen);
141
+ expression(item.expression, old.handlerNameSyntax);
142
+ requireValue(item.direct === (item.ancestors.length === 0), "return context mismatch");
143
+ if (item.expression.astKind === "Await")
144
+ expression(item.awaitedExpression, old.handlerNameSyntax);
145
+ else
146
+ requireValue(item.awaitedExpression === null, "unexpected awaited expression");
147
+ if (item.direct && !row.suite.truncated)
148
+ requireValue(row.suite.statements.some(statement => statement.kind === "Return" && statement.line === item.line && statement.endLine === item.endLine), "direct return absent from suite");
149
+ }
150
+ requireValue(seen.size <= 100000 && row.calls.length === original.bodyObservations.calls &&
151
+ isDeepStrictEqual(row.returns.map(item => ({ line: item.line, hasValue: item.expression.kind !== "absent" })), original.bodyObservations.returns), "owned call/return population mismatch");
152
+ requireValue(isDeepStrictEqual(row.classification, classification(row, old)), "classification mismatch");
153
+ }
154
+ requireValue(isDeepStrictEqual(report.summary, summary(report.handlers)), "summary/inventory mismatch");
155
+ }
@@ -0,0 +1,38 @@
1
+ import { type PythonErrorContext } from "./python-error-context.js";
2
+ type Context = PythonErrorContext["contexts"][number];
3
+ export interface Expression {
4
+ kind: string;
5
+ astKind: string | null;
6
+ symbol: string | null;
7
+ matchesHandlerNameSyntax: boolean;
8
+ }
9
+ interface Raise {
10
+ id: string;
11
+ line: number;
12
+ column: number;
13
+ endLine: number;
14
+ direct: boolean;
15
+ exception: Expression;
16
+ cause: Expression;
17
+ containsNamedExpression: boolean;
18
+ containers: {
19
+ kind: string;
20
+ line: number;
21
+ }[];
22
+ containersTruncated: boolean;
23
+ }
24
+ interface Handler extends Pick<Context, "handlerId" | "file" | "line" | "sourceSha256" | "population" | "owner" | "disposition"> {
25
+ handlerNameSyntax: string | null;
26
+ raises: Raise[];
27
+ originalClassification: Context["classification"];
28
+ classification: Context["classification"];
29
+ }
30
+ export interface PythonTransfers extends Omit<PythonErrorContext, "handlerReview" | "handlerRowsSha256" | "contexts"> {
31
+ contextReview: PythonErrorContext;
32
+ contextRowsSha256: string;
33
+ handlers: Handler[];
34
+ }
35
+ export declare function expression(value: Expression, handlerName: string | null): void;
36
+ /** Internal/source-bound consistency only, not AST remeasurement or authentication. Base must be validated first. */
37
+ export declare function validateTransfers(value: unknown, base: PythonErrorContext): asserts value is PythonTransfers;
38
+ export {};
@@ -0,0 +1,125 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ import { fingerprint } from "./python-error-context.js";
4
+ const policy = "python/exception-raise-chaining-v1";
5
+ function requireValue(value, message) {
6
+ if (!value)
7
+ throw new Error(`Invalid Python raise/chaining evidence: ${message}.`);
8
+ }
9
+ function keys(value, names) {
10
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
11
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), names.sort()), "unexpected or missing fields");
12
+ }
13
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
14
+ const nonempty = (value) => typeof value === "string" && value.trim().length > 0;
15
+ const name = (value) => typeof value === "string" && /^[_\p{ID_Start}][_\p{ID_Continue}]*$/u.test(value);
16
+ export function expression(value, handlerName) {
17
+ keys(value, ["kind", "astKind", "symbol", "matchesHandlerNameSyntax"]);
18
+ requireValue(["absent", "none", "name", "attribute", "call", "other"].includes(value.kind) && typeof value.matchesHandlerNameSyntax === "boolean", "invalid expression kind");
19
+ const expected = { absent: null, none: "Constant", name: "Name", attribute: "Attribute", call: "Call" };
20
+ requireValue(value.kind === "other" ? nonempty(value.astKind) && !["Name", "Attribute", "Call"].includes(value.astKind)
21
+ : value.astKind === expected[value.kind], "AST kind mismatch");
22
+ if (value.symbol !== null) {
23
+ requireValue(["name", "attribute", "call"].includes(value.kind) && typeof value.symbol === "string" && [...value.symbol].length <= 200 &&
24
+ value.symbol.split(".").length <= 9 && value.symbol.split(".").every(name), "invalid symbol path");
25
+ requireValue(value.kind !== "name" || !value.symbol.includes("."), "name cannot be a path");
26
+ requireValue(value.kind !== "attribute" || value.symbol.includes("."), "attribute needs a path");
27
+ }
28
+ // Long names intentionally have no bounded symbol, but can still match the original handler name.
29
+ requireValue(!value.matchesHandlerNameSyntax || value.kind === "name" && handlerName !== null && (value.symbol === null ? [...handlerName].length > 200 : value.symbol === handlerName), "handler-name correspondence mismatch");
30
+ if (value.kind === "name" && value.symbol !== null)
31
+ requireValue(value.matchesHandlerNameSyntax === (value.symbol === handlerName), "handler-name match omitted");
32
+ }
33
+ function classification(row, original) {
34
+ const prior = original.classification;
35
+ if (prior.status === "recognized-shape" || !isDeepStrictEqual(prior.unresolvedReasons, ["no-supported-local-shape"]) ||
36
+ original.handlerBody.statementCount !== 1 || original.handlerBody.statements[0].kind !== "Raise")
37
+ return prior;
38
+ requireValue(row.raises.length === 1 && row.raises[0].direct, "single raise suite mismatch");
39
+ const item = row.raises[0];
40
+ let pattern = null;
41
+ if (!item.containsNamedExpression && item.exception.symbol !== null) {
42
+ if (item.cause.kind === "none")
43
+ pattern = "direct-raise-from-none";
44
+ else if (item.cause.matchesHandlerNameSyntax)
45
+ pattern = "direct-raise-from-handler-name";
46
+ else if (item.cause.kind === "absent")
47
+ pattern = item.exception.matchesHandlerNameSyntax ? "direct-handler-name-raise" : "direct-symbol-or-call-raise";
48
+ }
49
+ return pattern ? { status: "recognized-shape", pattern, evidenceLines: [...new Set([row.line, item.line])].sort((a, b) => a - b), unresolvedReasons: [] }
50
+ : { status: "unresolved", pattern: null, evidenceLines: [row.line], unresolvedReasons: ["raise-expression-or-cause-unresolved"] };
51
+ }
52
+ const histogram = (values) => Object.fromEntries([...new Set(values)].sort().map(key => [key, values.filter(value => value === key).length]));
53
+ function summary(rows) {
54
+ const counts = (items) => {
55
+ const raises = items.flatMap(row => row.raises);
56
+ return { handlers: items.length, recognizedShapes: items.filter(row => row.classification.status === "recognized-shape").length,
57
+ unresolved: items.filter(row => row.classification.status === "unresolved").length,
58
+ byPattern: histogram(items.flatMap(row => row.classification.pattern ? [row.classification.pattern] : [])),
59
+ unresolvedReasons: histogram(items.flatMap(row => row.classification.unresolvedReasons)), raiseStatements: raises.length,
60
+ handlersWithRaises: items.filter(row => row.raises.length).length,
61
+ newlyRecognizedShapes: items.filter(row => row.originalClassification.status === "unresolved" && row.classification.status === "recognized-shape").length,
62
+ byExceptionSyntax: histogram(raises.map(row => row.exception.kind)), byCauseSyntax: histogram(raises.map(row => row.cause.kind)),
63
+ handlerNameRaiseSyntax: raises.filter(row => row.exception.matchesHandlerNameSyntax).length,
64
+ handlerNameCauseSyntax: raises.filter(row => row.cause.matchesHandlerNameSyntax).length };
65
+ };
66
+ return { ...counts(rows), populations: Object.fromEntries([...new Set(rows.map(row => row.population))].sort().map(kind => [kind, counts(rows.filter(row => row.population === kind))])) };
67
+ }
68
+ /** Internal/source-bound consistency only, not AST remeasurement or authentication. Base must be validated first. */
69
+ export function validateTransfers(value, base) {
70
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "contextReview", "contextRowsSha256", "status", "diagnostics", "handlers", "summary", "scope"]);
71
+ const report = value;
72
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-exception-transfer-review" && report.policy === policy &&
73
+ isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-exception-transfers", version: "0.1.0-experimental" }), "unsupported producer or policy");
74
+ requireValue(report.runId === base.runId && report.auditId === base.auditId && isDeepStrictEqual(report.contextReview, base) && report.contextRowsSha256 === fingerprint(base.contexts), "context provenance mismatch");
75
+ const configuration = { policy, contextPolicy: base.policy, contextConfigurationFingerprint: base.configurationFingerprint,
76
+ maxRaises: 100000, maxSymbolCharacters: 200, maxContainers: 32, maxSymbolAttributes: 8 };
77
+ requireValue(isDeepStrictEqual(report.configuration, configuration) && report.configurationFingerprint === fingerprint(configuration), "configuration mismatch");
78
+ requireValue(nonempty(report.scope) && typeof report.generatedAtUtc === "string" && Number.isFinite(Date.parse(report.generatedAtUtc)) && Array.isArray(report.handlers) && Array.isArray(report.diagnostics), "invalid metadata or collections");
79
+ for (const item of report.diagnostics) {
80
+ keys(item, item.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
81
+ requireValue(nonempty(item.code) && nonempty(item.message) && (item.file === undefined || nonempty(item.file)), "invalid diagnostic");
82
+ }
83
+ if (report.status === "incomplete") {
84
+ requireValue(report.diagnostics.length > 0 && !report.handlers.length && report.summary === null, "incomplete report must withhold rows");
85
+ return;
86
+ }
87
+ requireValue(report.status === "complete" && base.status === "complete" && !report.diagnostics.length && report.handlers.length === base.contexts.length, "status or population mismatch");
88
+ const contexts = new Map(base.contexts.map(row => [row.handlerId, row]));
89
+ const handlers = new Map(base.handlerReview.handlers.map(row => [row.id, row]));
90
+ const used = new Set(), raiseIds = new Set();
91
+ for (const row of report.handlers) {
92
+ keys(row, ["handlerId", "file", "line", "sourceSha256", "population", "owner", "handlerNameSyntax", "raises", "originalClassification", "classification", "disposition"]);
93
+ const original = contexts.get(row.handlerId), handler = handlers.get(row.handlerId);
94
+ requireValue(original && handler && !used.has(row.handlerId), "missing or repeated handler identity");
95
+ used.add(row.handlerId);
96
+ requireValue(["file", "line", "sourceSha256", "population", "owner"].every(key => isDeepStrictEqual(row[key], original[key])) &&
97
+ row.disposition === "review-only" && isDeepStrictEqual(row.originalClassification, original.classification) &&
98
+ (row.handlerNameSyntax === null || name(row.handlerNameSyntax)) && Array.isArray(row.raises), "handler/context mismatch");
99
+ for (const [index, item] of row.raises.entries()) {
100
+ keys(item, ["id", "line", "column", "endLine", "direct", "exception", "cause", "containsNamedExpression", "containers", "containersTruncated"]);
101
+ requireValue(integer(item.line) && item.line >= row.line && integer(item.endLine) && item.endLine >= item.line && item.endLine <= handler.endLine && integer(item.column) &&
102
+ (!index || item.line > row.raises[index - 1].line || item.line === row.raises[index - 1].line && item.column > row.raises[index - 1].column), "invalid raise location/order");
103
+ requireValue(item.id === hash(`${row.file}|${item.line}|${item.column}|Raise`) && !raiseIds.has(item.id), "raise identity mismatch");
104
+ raiseIds.add(item.id);
105
+ expression(item.exception, row.handlerNameSyntax);
106
+ expression(item.cause, row.handlerNameSyntax);
107
+ requireValue(item.exception.kind !== "absent" || item.cause.kind === "absent", "bare raise cannot have a cause");
108
+ requireValue(typeof item.direct === "boolean" && typeof item.containsNamedExpression === "boolean" && typeof item.containersTruncated === "boolean" &&
109
+ Array.isArray(item.containers) && item.containers.length <= 32 && (!item.containersTruncated || item.containers.length === 32) && item.direct === !item.containers.length, "invalid container accounting");
110
+ for (const [position, container] of item.containers.entries()) {
111
+ keys(container, ["kind", "line"]);
112
+ requireValue(["If", "For", "AsyncFor", "While", "Try", "TryStar", "With", "AsyncWith", "Match"].includes(container.kind) && integer(container.line) &&
113
+ container.line >= row.line && container.line <= item.line && (!position || container.line <= item.containers[position - 1].line), "invalid enclosing statement");
114
+ }
115
+ const direct = original.handlerBody.statements.find(statement => statement.kind === "Raise" && statement.line === item.line && statement.endLine === item.endLine);
116
+ requireValue(!direct || item.direct, "direct raise marked nested");
117
+ if (item.direct && !original.handlerBody.truncated)
118
+ requireValue(direct, "direct raise absent from suite");
119
+ }
120
+ const projected = row.raises.map(item => ({ line: item.line, bare: item.exception.kind === "absent", explicitCause: item.cause.kind !== "absent" }));
121
+ requireValue(isDeepStrictEqual(projected, handler.bodyObservations.raises), "owned raises disagree with original observations");
122
+ requireValue(isDeepStrictEqual(row.classification, classification(row, original)), "classification mismatch");
123
+ }
124
+ requireValue(raiseIds.size <= 100000 && isDeepStrictEqual(report.summary, summary(report.handlers)), "summary or budget mismatch");
125
+ }
@@ -0,0 +1,23 @@
1
+ import { type PythonAsyncHelpers, type PythonAsyncScoring } from "./python-async-scoring.js";
2
+ import { type PythonAsyncAdapters } from "./python-async-adapters.js";
3
+ import { type PythonAsyncContext } from "./python-async-context.js";
4
+ import { type PythonAsyncUsage } from "./python-async-usage.js";
5
+ import { type SourceFacts } from "#source-facts/contract.mjs";
6
+ import { type Evidence } from "./evidence.js";
7
+ import { type PythonDuplication } from "./python-duplication.js";
8
+ import { type PythonCoverage } from "./python-coverage.js";
9
+ import { type PythonDecomposition } from "./python-decomposition.js";
10
+ import { type PythonErrorHandling } from "./python-error-handling.js";
11
+ import { type PythonErrorContext } from "./python-error-context.js";
12
+ import { type PythonOperations } from "./python-error-operations.js";
13
+ import { type PythonTransfers } from "./python-error-transfers.js";
14
+ export declare const pythonVersion = "0.15.0";
15
+ export declare const pythonRuleset = "python-2026-10-28-async-scoring-v1";
16
+ export declare function pythonEvidence(facts: SourceFacts | null, subject: Evidence["subject"], ids: {
17
+ runId: string;
18
+ auditId: string;
19
+ }, error?: string, duplication?: PythonDuplication | null, coverage?: PythonCoverage | null, decomposition?: PythonDecomposition | null, errorHandling?: PythonErrorHandling | null, context?: PythonErrorContext | null, transfers?: PythonTransfers | null, operations?: PythonOperations | null, asyncReview?: PythonAsyncUsage | null, asyncContext?: PythonAsyncContext | null, asyncAdapters?: PythonAsyncAdapters | null, asyncHelpers?: PythonAsyncHelpers | null, asyncScoring?: PythonAsyncScoring | null): Evidence;
20
+ /** Validate supported scoring and historical review contracts within the shared v3 envelope. */
21
+ export declare function validatePythonEvidence(evidence: Evidence): void;
22
+ /** Historical previews and unavailable populations cannot participate in score comparisons. */
23
+ export declare function supportsPythonComparison(evidence: Evidence): boolean;