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,102 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ export const decompositionPolicy = "python/owned-statements-block-nesting-v1";
4
+ const blocks = ["If", "For", "AsyncFor", "While", "With", "AsyncWith", "Try", "TryStar", "Match"];
5
+ const statements = [...blocks, "Return", "Delete", "Assign", "AugAssign", "AnnAssign", "Raise", "Assert", "Import", "ImportFrom", "Expr", "Break", "Continue"];
6
+ const excluded = ["FunctionDef", "AsyncFunctionDef", "ClassDef", "Lambda", "Pass", "Global", "Nonlocal", "TypeAlias", "AnnAssign", "Expr", "docstring"];
7
+ function requireValue(condition, message) {
8
+ if (!condition)
9
+ throw new Error(`Invalid Python decomposition evidence: ${message}.`);
10
+ }
11
+ function keys(value, expected) {
12
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
13
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), expected.sort()), "unexpected or missing fields");
14
+ }
15
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
16
+ function histogram(value, allowed) {
17
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "histogram required");
18
+ const entries = Object.entries(value);
19
+ requireValue(entries.every(([key, count]) => allowed.includes(key) && integer(count) && count > 0), "invalid histogram");
20
+ const total = entries.reduce((sum, [, count]) => sum + count, 0);
21
+ requireValue(integer(total), "histogram overflow");
22
+ return total;
23
+ }
24
+ function distribution(values) {
25
+ if (!values.length)
26
+ return null;
27
+ values.sort((a, b) => a - b);
28
+ const rank = (percent) => values[Math.ceil(percent * values.length / 100) - 1];
29
+ return { count: values.length, p50: rank(50), p90: rank(90), p95: rank(95), max: values.at(-1) };
30
+ }
31
+ function summary(rows) {
32
+ const measured = rows.filter(row => row.status === "measured");
33
+ const populations = Object.fromEntries([...new Set(rows.map(row => row.population))].map(population => {
34
+ const selected = rows.filter(row => row.population === population);
35
+ const eligible = selected.filter(row => row.status === "measured");
36
+ return [population, { selectedFunctions: selected.length, measuredFunctions: eligible.length,
37
+ unavailableFunctions: selected.length - eligible.length,
38
+ ownedStatements: distribution(eligible.map(row => row.measurement.ownedStatements)),
39
+ maxBlockNesting: distribution(eligible.map(row => row.measurement.maxBlockNesting)) }];
40
+ }));
41
+ const unavailable = rows.length - measured.length;
42
+ return { selectedFunctions: rows.length, measuredFunctions: measured.length, unavailableFunctions: unavailable,
43
+ unavailableReasons: unavailable ? { "lambda-expression-body": unavailable } : {}, populations };
44
+ }
45
+ /** Validate provenance and internal accounting; AST measurements remain the producer's observations. */
46
+ export function validateDecomposition(value, facts, subject, ids) {
47
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "status", "policy", "configuration",
48
+ "configurationFingerprint", "subject", "sourceFactsProducer", "sourceFactsPolicy", "selection", "diagnostics", "scope", "functions", "summary"]);
49
+ const report = value;
50
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-decomposition-review" && report.policy === decompositionPolicy, "unsupported policy");
51
+ requireValue(isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-decomposition", version: "0.1.0-experimental" }), "producer mismatch");
52
+ requireValue(report.runId === ids.runId && report.auditId === ids.auditId, "stale invocation identities");
53
+ const target = subject.entryPoint.replaceAll("\\", "/").endsWith("/pyproject.toml") ? subject.root : subject.entryPoint;
54
+ requireValue(isDeepStrictEqual(report.subject, { root: subject.root, entryPoint: target }), "subject mismatch");
55
+ requireValue(isDeepStrictEqual(report.sourceFactsProducer, facts.producer) && report.sourceFactsPolicy === facts.measurementPolicy &&
56
+ isDeepStrictEqual(report.selection, facts.selection), "source selection/provenance mismatch");
57
+ const configuration = { policy: decompositionPolicy, selectionFingerprint: facts.selection.configurationFingerprint,
58
+ parserVersion: facts.producer.parserVersion, maxOwners: 100000, quantiles: "nearest-rank", complexityPolicy: facts.measurementPolicy };
59
+ requireValue(isDeepStrictEqual(report.configuration, configuration), "configuration mismatch");
60
+ requireValue(report.configurationFingerprint === hash(JSON.stringify(configuration, Object.keys(configuration).sort())), "configuration fingerprint mismatch");
61
+ requireValue(typeof report.scope === "string" && report.scope.length > 0 && typeof report.generatedAtUtc === "string" &&
62
+ Number.isFinite(Date.parse(report.generatedAtUtc)), "missing scope or generation time");
63
+ requireValue(Array.isArray(report.functions) && Array.isArray(report.diagnostics), "collections required");
64
+ for (const diagnostic of report.diagnostics) {
65
+ keys(diagnostic, diagnostic.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
66
+ requireValue(typeof diagnostic.code === "string" && typeof diagnostic.message === "string" &&
67
+ (diagnostic.file === undefined || typeof diagnostic.file === "string"), "invalid diagnostic");
68
+ }
69
+ if (report.status === "incomplete") {
70
+ requireValue(report.diagnostics.length > 0 && report.functions.length === 0 && report.summary === null, "incomplete results must withhold measurements");
71
+ return;
72
+ }
73
+ requireValue(report.status === "complete" && facts.status === "complete" && !report.diagnostics.length && report.functions.length <= 100000, "status or budget mismatch");
74
+ const owners = new Map(facts.functions.map(item => [item.id, item]));
75
+ const files = new Map(facts.selection.files.map(item => [item.file, item]));
76
+ const used = new Set();
77
+ for (const row of report.functions) {
78
+ keys(row, ["id", "file", "member", "line", "endLine", "population", "sourceSha256", "functionKind", "complexity", "disposition", "status", "measurement",
79
+ ...(row.status === "unavailable" ? ["reason"] : [])]);
80
+ const owner = owners.get(row.id);
81
+ requireValue(owner && !used.has(row.id), "unknown or repeated function");
82
+ used.add(row.id);
83
+ requireValue(row.file === owner.file && row.member === owner.member && row.line === owner.line && row.population === owner.population &&
84
+ row.sourceSha256 === files.get(row.file)?.contentSha256 && row.complexity === owner.complexity?.value &&
85
+ integer(row.endLine) && row.endLine >= row.line && row.disposition === "review-only", "location/source mismatch");
86
+ if (row.functionKind === "Lambda") {
87
+ requireValue(row.status === "unavailable" && row.measurement === null && row.reason === "lambda-expression-body", "lambda must be unavailable");
88
+ continue;
89
+ }
90
+ requireValue(["FunctionDef", "AsyncFunctionDef"].includes(row.functionKind) && row.status === "measured", "function measurement required");
91
+ keys(row.measurement, ["ownedStatements", "maxBlockNesting", "statementKinds", "excludedSyntax"]);
92
+ const measurement = row.measurement;
93
+ requireValue(integer(measurement.ownedStatements) && integer(measurement.maxBlockNesting), "invalid measurement");
94
+ requireValue(histogram(measurement.statementKinds, statements) === measurement.ownedStatements, "statement accounting mismatch");
95
+ histogram(measurement.excludedSyntax, excluded);
96
+ requireValue((measurement.excludedSyntax.docstring ?? 0) <= 1, "repeated leading docstring");
97
+ const blockCount = blocks.reduce((sum, kind) => sum + (measurement.statementKinds[kind] ?? 0), 0);
98
+ requireValue(measurement.maxBlockNesting <= blockCount && (measurement.maxBlockNesting === 0) === (blockCount === 0), "block accounting mismatch");
99
+ }
100
+ requireValue(used.size === owners.size, "missing selected functions");
101
+ requireValue(isDeepStrictEqual(report.summary, summary(report.functions)), "summary accounting mismatch");
102
+ }
@@ -0,0 +1,65 @@
1
+ import type { SourceFacts } from "#source-facts/contract.mjs";
2
+ import { type Evidence, type Finding } from "./evidence.js";
3
+ export declare const duplicationPolicy = "python/exact-function-body-v1";
4
+ interface Location {
5
+ id: string;
6
+ file: string;
7
+ member: string;
8
+ line: number;
9
+ endLine: number;
10
+ population: string;
11
+ statementCount: number | null;
12
+ sourceSha256: string;
13
+ }
14
+ interface Group {
15
+ id: string;
16
+ population: string;
17
+ functionKind: string;
18
+ bodySha256: string;
19
+ statementCount: number;
20
+ occurrences: Location[];
21
+ disposition: "review-only";
22
+ }
23
+ export interface PythonDuplication {
24
+ schemaVersion: number;
25
+ artifactKind: string;
26
+ producer: {
27
+ name: string;
28
+ version: string;
29
+ };
30
+ generatedAtUtc: string;
31
+ runId: string;
32
+ auditId: string;
33
+ status: "complete" | "incomplete";
34
+ policy: string;
35
+ configuration: Record<string, string | number>;
36
+ configurationFingerprint: string;
37
+ subject: {
38
+ root: string;
39
+ entryPoint: string;
40
+ };
41
+ sourceFactsProducer: SourceFacts["producer"];
42
+ sourceFactsPolicy: string;
43
+ selection: SourceFacts["selection"];
44
+ scope: string;
45
+ diagnostics: SourceFacts["diagnostics"];
46
+ groups: Group[];
47
+ excludedFunctions: (Location & {
48
+ reason: string;
49
+ })[];
50
+ summary: {
51
+ selectedFunctions: number;
52
+ eligibleFunctions: number;
53
+ excludedFunctions: number;
54
+ exclusionReasons: Record<string, number>;
55
+ duplicateGroups: number;
56
+ matchedFunctions: number;
57
+ } | null;
58
+ }
59
+ /** Check provenance, population membership and accounting; source equality is a producer measurement. */
60
+ export declare function validateDuplication(value: unknown, facts: SourceFacts, subject: Evidence["subject"], ids: {
61
+ runId: string;
62
+ auditId: string;
63
+ }): asserts value is PythonDuplication;
64
+ export declare function duplicationFindings(report: PythonDuplication): Finding[];
65
+ export {};
@@ -0,0 +1,89 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ export const duplicationPolicy = "python/exact-function-body-v1";
4
+ function requireValue(condition, message) {
5
+ if (!condition)
6
+ throw new Error(`Invalid Python duplication evidence: ${message}.`);
7
+ }
8
+ function keys(value, expected) {
9
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
10
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), expected.sort()), "unexpected or missing fields");
11
+ }
12
+ const hex = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
13
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
14
+ /** Check provenance, population membership and accounting; source equality is a producer measurement. */
15
+ export function validateDuplication(value, facts, subject, ids) {
16
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "status", "policy", "configuration",
17
+ "configurationFingerprint", "subject", "sourceFactsProducer", "sourceFactsPolicy", "selection", "diagnostics", "scope", "groups", "excludedFunctions", "summary"]);
18
+ const report = value;
19
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-duplication-review" && report.policy === duplicationPolicy, "unsupported policy");
20
+ requireValue(isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-duplication", version: "0.1.0-experimental" }), "producer mismatch");
21
+ requireValue(report.runId === ids.runId && report.auditId === ids.auditId, "stale invocation identities");
22
+ const target = subject.entryPoint.replaceAll("\\", "/").endsWith("/pyproject.toml") ? subject.root : subject.entryPoint;
23
+ requireValue(isDeepStrictEqual(report.subject, { root: subject.root, entryPoint: target }), "subject mismatch");
24
+ requireValue(isDeepStrictEqual(report.sourceFactsProducer, facts.producer) && report.sourceFactsPolicy === facts.measurementPolicy &&
25
+ isDeepStrictEqual(report.selection, facts.selection), "source selection/provenance mismatch");
26
+ const configuration = { policy: duplicationPolicy, minimumStatements: 5, selectionFingerprint: facts.selection.configurationFingerprint,
27
+ parserVersion: facts.producer.parserVersion, maxOwners: 100000, maxNormalizedBytes: 67108864 };
28
+ requireValue(isDeepStrictEqual(report.configuration, configuration), "configuration mismatch");
29
+ requireValue(report.configurationFingerprint === hash(JSON.stringify(configuration, Object.keys(configuration).sort())), "configuration fingerprint mismatch");
30
+ requireValue(typeof report.scope === "string" && report.scope.length > 0 && typeof report.generatedAtUtc === "string" &&
31
+ Number.isFinite(Date.parse(report.generatedAtUtc)), "missing scope or generation time");
32
+ requireValue(Array.isArray(report.groups) && Array.isArray(report.excludedFunctions) && Array.isArray(report.diagnostics), "collections required");
33
+ for (const diagnostic of report.diagnostics) {
34
+ keys(diagnostic, diagnostic.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
35
+ requireValue(typeof diagnostic.code === "string" && typeof diagnostic.message === "string" &&
36
+ (diagnostic.file === undefined || typeof diagnostic.file === "string"), "invalid diagnostic");
37
+ }
38
+ if (report.status === "incomplete") {
39
+ requireValue(report.diagnostics.length > 0 && report.groups.length === 0 && report.excludedFunctions.length === 0 && report.summary === null, "incomplete results must withhold groups and summary");
40
+ return;
41
+ }
42
+ requireValue(report.status === "complete" && facts.status === "complete" && report.diagnostics.length === 0, "status mismatch");
43
+ const owners = new Map(facts.functions.map(item => [item.id, item]));
44
+ const files = new Map(facts.selection.files.map(item => [item.file, item]));
45
+ const used = new Set(), groupIds = new Set(), bodies = new Set();
46
+ function location(item, excluded) {
47
+ keys(item, ["id", "file", "member", "line", "endLine", "population", "statementCount", "sourceSha256", ...(excluded ? ["reason"] : [])]);
48
+ const owner = owners.get(item.id);
49
+ requireValue(owner && !used.has(item.id), "unknown or repeated function");
50
+ used.add(item.id);
51
+ requireValue(item.file === owner.file && item.member === owner.member && item.line === owner.line && item.population === owner.population &&
52
+ item.sourceSha256 === files.get(item.file)?.contentSha256 && integer(item.endLine) && item.endLine >= item.line, "location/source mismatch");
53
+ }
54
+ const reasons = {};
55
+ for (const item of report.excludedFunctions) {
56
+ location(item, true);
57
+ requireValue(item.reason === "below-statement-minimum" ? integer(item.statementCount) && item.statementCount < 5 :
58
+ ["lambda", "nested-definition"].includes(item.reason) && item.statementCount === null, "exclusion mismatch");
59
+ reasons[item.reason] = (reasons[item.reason] ?? 0) + 1;
60
+ }
61
+ let matched = 0;
62
+ for (const group of report.groups) {
63
+ keys(group, ["id", "population", "functionKind", "bodySha256", "statementCount", "occurrences", "disposition"]);
64
+ requireValue(hex(group.id) && !groupIds.has(group.id) && hex(group.bodySha256), "invalid or repeated group identity");
65
+ groupIds.add(group.id);
66
+ const body = JSON.stringify([group.population, group.functionKind, group.bodySha256]);
67
+ requireValue(!bodies.has(body), "repeated body group");
68
+ bodies.add(body);
69
+ requireValue(["FunctionDef", "AsyncFunctionDef"].includes(group.functionKind) && group.disposition === "review-only" &&
70
+ integer(group.statementCount) && group.statementCount >= 5 && Array.isArray(group.occurrences) && group.occurrences.length >= 2, "invalid group");
71
+ for (const item of group.occurrences) {
72
+ location(item, false);
73
+ requireValue(item.population === group.population && item.statementCount === group.statementCount, "mixed group population/count");
74
+ }
75
+ matched += group.occurrences.length;
76
+ }
77
+ requireValue(isDeepStrictEqual(report.summary, { selectedFunctions: facts.functions.length, eligibleFunctions: facts.functions.length - report.excludedFunctions.length,
78
+ excludedFunctions: report.excludedFunctions.length, exclusionReasons: reasons, duplicateGroups: report.groups.length, matchedFunctions: matched }), "summary accounting mismatch");
79
+ }
80
+ export function duplicationFindings(report) {
81
+ return report.groups.map(group => {
82
+ const first = group.occurrences[0];
83
+ return { category: "exactBodyDuplicationReview", ruleId: "python/duplication/exact-function-body-v1",
84
+ fingerprint: group.id, severity: "info", confidence: "high", file: first.file, line: first.line, member: first.member,
85
+ message: `${group.occurrences.length} matching whole-function bodies (${group.statementCount} statements each); review context before suggesting extraction.`,
86
+ observations: { groupId: group.id, policy: report.policy, occurrences: group.occurrences, statementCount: group.statementCount,
87
+ scoringDisposition: "unscored", semanticEquivalence: "not-established" } };
88
+ });
89
+ }
@@ -0,0 +1,84 @@
1
+ import type { PythonErrorHandling } from "./python-error-handling.js";
2
+ export declare const contextPolicy = "python/exception-context-patterns-v1";
3
+ type Handler = PythonErrorHandling["handlers"][number];
4
+ interface Statement {
5
+ kind: string;
6
+ line: number;
7
+ endLine: number;
8
+ literalReturnKind: string | null;
9
+ literalAssignment: {
10
+ targets: string[];
11
+ valueKind: string;
12
+ } | null;
13
+ }
14
+ interface Suite {
15
+ statementCount: number;
16
+ statements: Statement[];
17
+ truncated: boolean;
18
+ }
19
+ interface Context {
20
+ handlerId: string;
21
+ file: string;
22
+ line: number;
23
+ sourceSha256: string;
24
+ population: string;
25
+ owner: Handler["owner"];
26
+ tryStatement: {
27
+ line: number;
28
+ endLine: number;
29
+ kind: string;
30
+ body: Suite;
31
+ else: Suite;
32
+ finally: Suite;
33
+ handlerLines: number[];
34
+ };
35
+ handlerBody: Suite;
36
+ followingInSameSuite: Suite & {
37
+ suite: string;
38
+ };
39
+ enclosingConstructs: {
40
+ kind: string;
41
+ line: number;
42
+ }[];
43
+ surroundingDocumentation: {
44
+ items: {
45
+ kind: string;
46
+ line: number;
47
+ text: string;
48
+ truncated: boolean;
49
+ }[];
50
+ truncated: boolean;
51
+ };
52
+ classification: {
53
+ status: string;
54
+ pattern: string | null;
55
+ evidenceLines: number[];
56
+ unresolvedReasons: string[];
57
+ };
58
+ disposition: string;
59
+ }
60
+ export interface PythonErrorContext {
61
+ schemaVersion: number;
62
+ artifactKind: string;
63
+ producer: {
64
+ name: string;
65
+ version: string;
66
+ };
67
+ generatedAtUtc: string;
68
+ runId: string;
69
+ auditId: string;
70
+ policy: string;
71
+ configuration: Record<string, string | number>;
72
+ configurationFingerprint: string;
73
+ handlerReview: PythonErrorHandling;
74
+ handlerRowsSha256: string;
75
+ status: string;
76
+ diagnostics: PythonErrorHandling["diagnostics"];
77
+ contexts: Context[];
78
+ summary: unknown;
79
+ scope: string;
80
+ }
81
+ export declare function fingerprint(value: unknown): string;
82
+ /** Validate source-bound consistency; syntax is not independently remeasured or authenticated. */
83
+ export declare function validateErrorContext(value: unknown, base: PythonErrorHandling): asserts value is PythonErrorContext;
84
+ export {};
@@ -0,0 +1,165 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ export const contextPolicy = "python/exception-context-patterns-v1";
4
+ function requireValue(value, message) {
5
+ if (!value)
6
+ throw new Error(`Invalid Python exception context: ${message}.`);
7
+ }
8
+ function keys(value, names) {
9
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
10
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), names.sort()), "unexpected or missing fields");
11
+ }
12
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
13
+ const nonempty = (value) => typeof value === "string" && value.trim().length > 0;
14
+ const literalKinds = ["none", "bool", "int", "float", "complex", "str", "bytes"];
15
+ const statementKinds = ["FunctionDef", "AsyncFunctionDef", "ClassDef", "Return", "Delete", "Assign", "TypeAlias", "AugAssign", "AnnAssign", "For", "AsyncFor", "While", "If", "With", "AsyncWith", "Match", "Raise", "Try", "TryStar", "Assert", "Import", "ImportFrom", "Global", "Nonlocal", "Expr", "Pass", "Break", "Continue"];
16
+ const patterns = ["locally-documented-no-op", "direct-literal-fallback-return", "direct-bare-rethrow", "guarded-import-shape", "guarded-import-literal-default", "no-op-then-literal-return"];
17
+ const reasons = ["exception-group-semantics", "enclosing-finally", "enclosing-context-manager", "nested-handler", "import-exception-binding-unresolved", "no-supported-local-shape"];
18
+ // Match Python json.dumps(sort_keys=True, ensure_ascii=True, separators=(",", ":")) for this integer/string contract.
19
+ export function fingerprint(value) {
20
+ const json = JSON.stringify(value, (_key, item) => item !== null && typeof item === "object" && !Array.isArray(item)
21
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item);
22
+ return hash(json.replace(/[\u007f-\uffff]/g, character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`));
23
+ }
24
+ function suite(value, maximum, start, end, following = false) {
25
+ keys(value, ["statementCount", "statements", "truncated", ...(following ? ["suite"] : [])]);
26
+ requireValue(integer(value.statementCount) && Array.isArray(value.statements) && value.statements.length === Math.min(maximum, value.statementCount) &&
27
+ value.truncated === (value.statementCount > maximum), "suite accounting mismatch");
28
+ for (const [index, item] of value.statements.entries()) {
29
+ keys(item, ["kind", "line", "endLine", "literalReturnKind", "literalAssignment"]);
30
+ requireValue(statementKinds.includes(item.kind) && integer(item.line) && item.line >= start && integer(item.endLine) && item.endLine >= item.line && item.endLine <= end &&
31
+ (!index || item.line >= value.statements[index - 1].endLine), "invalid statement location or kind");
32
+ requireValue(item.literalReturnKind === null || item.kind === "Return" && literalKinds.includes(item.literalReturnKind), "invalid literal return");
33
+ if (item.literalAssignment !== null) {
34
+ keys(item.literalAssignment, ["targets", "valueKind"]);
35
+ requireValue(item.kind === "Assign" && literalKinds.includes(item.literalAssignment.valueKind) && Array.isArray(item.literalAssignment.targets) &&
36
+ item.literalAssignment.targets.length > 0 && item.literalAssignment.targets.every(nonempty), "invalid literal assignment");
37
+ }
38
+ }
39
+ }
40
+ function documentation(value, row) {
41
+ keys(value, ["items", "truncated"]);
42
+ requireValue(Array.isArray(value.items) && typeof value.truncated === "boolean", "documentation required");
43
+ let length = 0;
44
+ for (const item of value.items) {
45
+ keys(item, ["kind", "line", "text", "truncated"]);
46
+ requireValue(["preceding-try-comment", "owner-docstring"].includes(item.kind) && integer(item.line) && item.line > 0 && item.line < row.tryStatement.line &&
47
+ typeof item.text === "string" && item.text.length > 0 && typeof item.truncated === "boolean", "invalid surrounding prose");
48
+ length += [...item.text].length;
49
+ }
50
+ requireValue(length <= 2000 && (!value.items.some(item => item.truncated) || value.truncated), "documentation budget mismatch");
51
+ }
52
+ function classification(row, handler) {
53
+ const value = row.classification;
54
+ keys(value, ["status", "pattern", "evidenceLines", "unresolvedReasons"]);
55
+ requireValue(Array.isArray(value.evidenceLines) && value.evidenceLines.length > 0 && value.evidenceLines.every(line => integer(line) && line > 0) &&
56
+ Array.isArray(value.unresolvedReasons) && value.unresolvedReasons.every(reason => reasons.includes(reason)) && new Set(value.unresolvedReasons).size === value.unresolvedReasons.length, "invalid classification evidence");
57
+ const blockers = [
58
+ ...(handler.exceptionGroup ? ["exception-group-semantics"] : []),
59
+ ...(handler.enclosingFinallyLines.length ? ["enclosing-finally"] : []),
60
+ ...(row.enclosingConstructs.some(item => ["With", "AsyncWith"].includes(item.kind)) ? ["enclosing-context-manager"] : []),
61
+ ...(row.enclosingConstructs.some(item => item.kind === "ExceptHandler") ? ["nested-handler"] : [])
62
+ ];
63
+ if (value.status === "unresolved") {
64
+ requireValue(value.pattern === null && value.unresolvedReasons.length > 0 && isDeepStrictEqual(value.evidenceLines, [row.line]) &&
65
+ (blockers.length ? isDeepStrictEqual(value.unresolvedReasons, blockers) : value.unresolvedReasons.length === 1 && ["import-exception-binding-unresolved", "no-supported-local-shape"].includes(value.unresolvedReasons[0])), "unresolved classification mismatch");
66
+ requireValue(handler.classification !== "documented-no-op", "local documentation pattern missing");
67
+ return;
68
+ }
69
+ requireValue(value.status === "recognized-shape" && patterns.includes(value.pattern) && !value.unresolvedReasons.length, "invalid pattern");
70
+ const first = row.handlerBody.statements[0];
71
+ const imported = row.tryStatement.body;
72
+ let lines = [];
73
+ if (value.pattern === "locally-documented-no-op") {
74
+ requireValue(handler.classification === "documented-no-op", "documentation pattern mismatch");
75
+ lines = handler.documentation.map(item => item.line);
76
+ }
77
+ else {
78
+ requireValue(!blockers.length && handler.classification !== "documented-no-op", "pattern bypasses context or documentation precedence");
79
+ if (value.pattern === "direct-literal-fallback-return") {
80
+ requireValue(row.handlerBody.statementCount === 1 && first.literalReturnKind !== null && handler.classification === "terminal-return", "fallback pattern mismatch");
81
+ lines = [row.line, first.line];
82
+ }
83
+ else if (value.pattern === "direct-bare-rethrow") {
84
+ requireValue(row.handlerBody.statementCount === 1 && first.kind === "Raise" && handler.bodyObservations.raises.length === 1 && handler.bodyObservations.raises[0].bare, "rethrow pattern mismatch");
85
+ lines = [row.line, first.line];
86
+ }
87
+ else if (value.pattern === "no-op-then-literal-return") {
88
+ requireValue(handler.classification === "undocumented-no-op" && row.followingInSameSuite.statements[0]?.literalReturnKind != null, "fallthrough pattern mismatch");
89
+ lines = [row.line, row.followingInSameSuite.statements[0].line];
90
+ }
91
+ else {
92
+ requireValue(imported.statementCount === 1 && ["Import", "ImportFrom"].includes(imported.statements[0].kind) &&
93
+ /^(?:ImportError|ModuleNotFoundError|\((?:ImportError|ModuleNotFoundError)(?:, (?:ImportError|ModuleNotFoundError))*,?\))$/.test(handler.exceptionTypeSyntax ?? ""), "import pattern mismatch");
94
+ lines = [imported.statements[0].line, row.line];
95
+ if (value.pattern === "guarded-import-shape")
96
+ requireValue(handler.classification === "undocumented-no-op", "import no-op mismatch");
97
+ else {
98
+ requireValue(row.handlerBody.statementCount === 1 && first.literalAssignment !== null, "import default mismatch");
99
+ lines.push(first.line);
100
+ }
101
+ }
102
+ lines = [...new Set(lines)].sort((a, b) => a - b);
103
+ }
104
+ requireValue(isDeepStrictEqual(value.evidenceLines, lines), "pattern evidence locations mismatch");
105
+ }
106
+ function histogram(values) {
107
+ return Object.fromEntries([...new Set(values)].sort().map(key => [key, values.filter(value => value === key).length]));
108
+ }
109
+ function summary(rows) {
110
+ const counts = (items) => ({ handlers: items.length, recognizedShapes: items.filter(row => row.classification.status === "recognized-shape").length,
111
+ unresolved: items.filter(row => row.classification.status === "unresolved").length,
112
+ byPattern: histogram(items.flatMap(row => row.classification.pattern ? [row.classification.pattern] : [])),
113
+ unresolvedReasons: histogram(items.flatMap(row => row.classification.unresolvedReasons)) });
114
+ return { ...counts(rows), populations: Object.fromEntries([...new Set(rows.map(row => row.population))].sort().map(kind => [kind, counts(rows.filter(row => row.population === kind))])) };
115
+ }
116
+ /** Validate source-bound consistency; syntax is not independently remeasured or authenticated. */
117
+ export function validateErrorContext(value, base) {
118
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "handlerReview", "handlerRowsSha256", "status", "diagnostics", "contexts", "summary", "scope"]);
119
+ const report = value;
120
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-exception-context-review" && report.policy === contextPolicy &&
121
+ isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-exception-context", version: "0.1.0-experimental" }), "unsupported producer or policy");
122
+ requireValue(report.runId === base.runId && report.auditId === base.auditId && isDeepStrictEqual(report.handlerReview, base) && report.handlerRowsSha256 === fingerprint(base.handlers), "handler provenance mismatch");
123
+ const configuration = { policy: contextPolicy, handlerPolicy: base.policy, maxStatementsPerSuite: 32, maxFollowingStatements: 3,
124
+ maxDocumentationCharacters: 2000, selectionFingerprint: base.selection.configurationFingerprint, parserVersion: base.sourceFactsProducer.parserVersion };
125
+ requireValue(isDeepStrictEqual(report.configuration, configuration) && report.configurationFingerprint === fingerprint(configuration), "configuration mismatch");
126
+ requireValue(nonempty(report.scope) && typeof report.generatedAtUtc === "string" && Number.isFinite(Date.parse(report.generatedAtUtc)) && Array.isArray(report.contexts) && Array.isArray(report.diagnostics), "missing metadata or collections");
127
+ for (const diagnostic of report.diagnostics) {
128
+ keys(diagnostic, diagnostic.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
129
+ requireValue(nonempty(diagnostic.code) && nonempty(diagnostic.message) && (diagnostic.file === undefined || nonempty(diagnostic.file)), "invalid diagnostic");
130
+ }
131
+ if (report.status === "incomplete") {
132
+ requireValue(report.diagnostics.length > 0 && !report.contexts.length && report.summary === null, "incomplete results must withhold contexts");
133
+ return;
134
+ }
135
+ requireValue(report.status === "complete" && base.status === "complete" && !report.diagnostics.length && report.contexts.length === base.handlers.length, "status or population mismatch");
136
+ const handlers = new Map(base.handlers.map(row => [row.id, row]));
137
+ const used = new Set();
138
+ for (const row of report.contexts) {
139
+ keys(row, ["handlerId", "file", "line", "sourceSha256", "population", "owner", "tryStatement", "handlerBody", "followingInSameSuite", "enclosingConstructs", "surroundingDocumentation", "classification", "disposition"]);
140
+ const handler = handlers.get(row.handlerId);
141
+ requireValue(handler && !used.has(row.handlerId), "missing or repeated handler identity");
142
+ used.add(row.handlerId);
143
+ requireValue(row.file === handler.file && row.line === handler.line && row.sourceSha256 === handler.sourceSha256 && row.population === handler.population &&
144
+ isDeepStrictEqual(row.owner, handler.owner) && row.disposition === "review-only", "handler location/source mismatch");
145
+ const attempt = row.tryStatement;
146
+ keys(attempt, ["line", "endLine", "kind", "body", "else", "finally", "handlerLines"]);
147
+ requireValue(integer(attempt.line) && attempt.line > 0 && attempt.line < row.line && integer(attempt.endLine) && attempt.endLine >= handler.endLine &&
148
+ attempt.kind === (handler.exceptionGroup ? "TryStar" : "Try") && Array.isArray(attempt.handlerLines) && attempt.handlerLines.includes(row.line) &&
149
+ attempt.handlerLines.every((line, index, list) => integer(line) && line > attempt.line && line <= attempt.endLine && (!index || line > list[index - 1])), "invalid containing try");
150
+ for (const key of ["body", "else", "finally"])
151
+ suite(attempt[key], 32, attempt.line, attempt.endLine);
152
+ requireValue(attempt.body.statementCount > 0 && (attempt.finally.statementCount > 0) === handler.enclosingFinallyLines.includes(attempt.line), "try/finally mismatch");
153
+ suite(row.handlerBody, 32, handler.line, handler.endLine);
154
+ requireValue(row.handlerBody.statementCount > 0, "empty handler suite");
155
+ suite(row.followingInSameSuite, 3, attempt.endLine, Number.MAX_SAFE_INTEGER, true);
156
+ requireValue(["body", "orelse", "finalbody"].includes(row.followingInSameSuite.suite), "invalid containing suite");
157
+ requireValue(Array.isArray(row.enclosingConstructs) && row.enclosingConstructs.every(item => {
158
+ keys(item, ["kind", "line"]);
159
+ return [...statementKinds, "ExceptHandler"].includes(item.kind) && integer(item.line) && item.line > 0 && item.line < row.line;
160
+ }), "invalid ancestors");
161
+ documentation(row.surroundingDocumentation, row);
162
+ classification(row, handler);
163
+ }
164
+ requireValue(isDeepStrictEqual(report.summary, summary(report.contexts)), "summary accounting mismatch");
165
+ }
@@ -0,0 +1,56 @@
1
+ import type { SourceFacts } from "#source-facts/contract.mjs";
2
+ import { type Evidence } from "./evidence.js";
3
+ import type { PythonDecomposition } from "./python-decomposition.js";
4
+ export declare const errorHandlingPolicy = "python/exception-handler-observations-v1";
5
+ interface Annotation {
6
+ kind: string;
7
+ line: number;
8
+ text: string;
9
+ }
10
+ interface Handler {
11
+ id: string;
12
+ file: string;
13
+ line: number;
14
+ endLine: number;
15
+ sourceSha256: string;
16
+ population: string;
17
+ owner: {
18
+ kind: string;
19
+ member: string;
20
+ functionId: string | null;
21
+ };
22
+ exceptionTypeSyntax: string | null;
23
+ bareExcept: boolean;
24
+ exceptionGroup: boolean;
25
+ enclosingFinallyLines: number[];
26
+ classification: string;
27
+ documentation: Annotation[];
28
+ directiveComments: Annotation[];
29
+ disposition: string;
30
+ bodyObservations: {
31
+ raises: {
32
+ line: number;
33
+ bare: boolean;
34
+ explicitCause: boolean;
35
+ }[];
36
+ returns: {
37
+ line: number;
38
+ hasValue: boolean;
39
+ }[];
40
+ loopTransfers: {
41
+ line: number;
42
+ kind: string;
43
+ }[];
44
+ calls: number;
45
+ nestedHandlersExcluded: number;
46
+ };
47
+ }
48
+ export interface PythonErrorHandling extends Omit<PythonDecomposition, "functions"> {
49
+ handlers: Handler[];
50
+ }
51
+ /** Checks provenance and internal consistency, not independent AST remeasurement or authentication. */
52
+ export declare function validateErrorHandling(value: unknown, facts: SourceFacts, subject: Evidence["subject"], ids: {
53
+ runId: string;
54
+ auditId: string;
55
+ }): asserts value is PythonErrorHandling;
56
+ export {};