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,143 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { fingerprint } from "./python-error-context.js";
3
+ export const asyncUsagePolicy = "python/async-owned-calls-review-v1";
4
+ const blocking = ["os.system", "subprocess.call", "subprocess.check_call", "subprocess.check_output", "subprocess.run", "time.sleep", "urllib.request.urlopen"];
5
+ const offload = ["anyio.to_thread.run_sync", "asyncio.to_thread", "starlette.concurrency.run_in_threadpool"];
6
+ const wrapper = "asgiref.sync.sync_to_async";
7
+ const unresolved = ["dynamic-callee-expression", "comprehension-local-binding", "reflective-or-cross-scope-binding", "non-import-or-ambiguous-binding", "import-not-before-use", "imported-module-attribute-write", "no-lexical-import"];
8
+ const expressionKinds = ["BoolOp", "NamedExpr", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict", "Set", "ListComp", "SetComp", "DictComp", "GeneratorExp", "Await", "Yield", "YieldFrom", "Compare", "Call", "FormattedValue", "JoinedStr", "Constant", "Attribute", "Subscript", "Name", "List", "Tuple"];
9
+ function requireValue(condition, message) {
10
+ if (!condition)
11
+ throw new Error(`Invalid Python async evidence: ${message}.`);
12
+ }
13
+ function keys(value, names) {
14
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "object required");
15
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), names.sort()), "unexpected or missing fields");
16
+ }
17
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
18
+ const nonempty = (value) => typeof value === "string" && value.trim().length > 0;
19
+ const pathSyntax = (value) => nonempty(value) && value.split(".").every(part => part.trim() === part && part.length > 0 && !/[\s()\[\]]/u.test(part));
20
+ const histogram = (items) => Object.fromEntries([...new Set(items)].sort().map(key => [key, items.filter(value => value === key).length]));
21
+ function binding(value, wrapped = false) {
22
+ keys(value, ["syntax", "target", "status", "reason"]);
23
+ requireValue(value.syntax === null || pathSyntax(value.syntax), "invalid callee spelling");
24
+ if (value.status === "import-matched") {
25
+ requireValue(pathSyntax(value.target) && value.syntax !== null, "matched import must have a target and spelling");
26
+ requireValue(value.reason === (wrapped ? "immediate-import-matched-wrapper-syntax" : "import-spelling-only"), "invalid match reason");
27
+ }
28
+ else {
29
+ requireValue(!wrapped && value.status === "unresolved" && value.target === null && unresolved.includes(value.reason), "invalid unresolved binding");
30
+ requireValue((value.syntax === null) === (value.reason === "dynamic-callee-expression"), "binding syntax/reason mismatch");
31
+ }
32
+ }
33
+ function callback(value) {
34
+ requireValue(value !== null, "helper callback evidence required");
35
+ if (value.kind === "unresolved-argument-layout") {
36
+ keys(value, ["kind", "binding"]);
37
+ requireValue(value.binding === null, "unresolved arguments cannot resolve a callback");
38
+ return;
39
+ }
40
+ keys(value, ["kind", "binding", "evaluation"]);
41
+ requireValue(expressionKinds.includes(value.kind) && value.binding !== null, "invalid callback expression");
42
+ binding(value.binding);
43
+ requireValue(value.evaluation === (value.kind === "Call" ? "eager-call-expression" : "callback-expression-only"), "callback evaluation mismatch");
44
+ requireValue((value.binding.syntax !== null) === ["Name", "Attribute"].includes(value.kind) ||
45
+ value.kind === "Attribute" && value.binding.syntax === null, "callback spelling/kind mismatch");
46
+ }
47
+ function counts(rows) {
48
+ const eligible = rows.filter(row => row.status === "observed");
49
+ const calls = eligible.flatMap(row => row.measurement.calls);
50
+ const direct = calls.filter(call => call.executionContext === "async-body");
51
+ return { selectedFunctions: rows.length, asyncFunctions: eligible.length, excludedSynchronousFunctions: rows.length - eligible.length,
52
+ availability: eligible.length ? "available" : "unavailable", asyncBodyCallExpressions: eligible.length ? direct.length : null,
53
+ asyncBodyCallCategories: eligible.length ? histogram(direct.map(call => call.category)) : null,
54
+ deferredGeneratorCallExpressions: eligible.length ? calls.length - direct.length : null,
55
+ functionsWithBlockingCandidates: eligible.length ? eligible.filter(row => row.measurement.calls.some(call => call.category === "blocking-call-candidate" && call.executionContext === "async-body")).length : null };
56
+ }
57
+ function callEvidence(call, row, used) {
58
+ keys(call, ["id", "line", "column", "endLine", "endColumn", "executionContext", "category", "binding", "directlyAwaited", "callback", "disposition"]);
59
+ requireValue([call.line, call.column, call.endLine, call.endColumn].every(integer) && call.line >= row.line && call.endLine <= row.endLine &&
60
+ (call.endLine > call.line || call.endLine === call.line && call.endColumn > call.column), "invalid call extent");
61
+ const location = { line: call.line, column: call.column, endLine: call.endLine, endColumn: call.endColumn };
62
+ requireValue(call.id === fingerprint([row.id, location]) && !used.has(call.id), "call identity mismatch or duplicate");
63
+ used.add(call.id);
64
+ requireValue(["async-body", "deferred-generator-body"].includes(call.executionContext) && typeof call.directlyAwaited === "boolean" && call.disposition === "review-only", "call context/disposition mismatch");
65
+ const wrapped = call.category === "offload-wrapped-invocation";
66
+ binding(call.binding, wrapped);
67
+ const target = call.binding.target;
68
+ const category = target === null ? "unresolved-call" : blocking.includes(target) ? "blocking-call-candidate" :
69
+ offload.includes(target) ? "offload-call" : target === wrapper ? (wrapped ? "offload-wrapped-invocation" : "offload-wrapper-construction") : "other-import-call";
70
+ requireValue(call.category === category, "call category does not match the catalogue");
71
+ if (call.category.startsWith("offload-")) {
72
+ requireValue(call.callback !== null, "helper callback required");
73
+ callback(call.callback);
74
+ if (wrapped) {
75
+ requireValue(call.callback.kind !== "unresolved-argument-layout", "wrapped invocation requires a direct positional callback");
76
+ requireValue(row.measurement.calls.some(inner => inner.category === "offload-wrapper-construction" &&
77
+ inner.line === call.line && inner.column === call.column &&
78
+ (inner.endLine < call.endLine || inner.endLine === call.endLine && inner.endColumn < call.endColumn) &&
79
+ inner.executionContext === call.executionContext && !inner.directlyAwaited && isDeepStrictEqual(inner.callback, call.callback)), "missing immediate wrapper construction");
80
+ }
81
+ }
82
+ else
83
+ requireValue(call.callback === null, "non-helper cannot claim an offloaded callback");
84
+ }
85
+ /** Validate identity, catalogue coherence and accounting, not runtime resolution or independent AST semantics. */
86
+ export function validateAsyncUsage(value, facts, subject, ids, decomposition) {
87
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "subject", "sourceFactsProducer", "sourceFactsPolicy", "selection", "status", "diagnostics", "scope", "functions", "summary"]);
88
+ const report = value;
89
+ requireValue(report.schemaVersion === 1 && report.artifactKind === "experimental-python-async-usage-review" && report.policy === asyncUsagePolicy &&
90
+ isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-async-usage", version: "0.1.0-experimental" }), "unsupported producer/policy");
91
+ requireValue(report.runId === ids.runId && report.auditId === ids.auditId, "stale invocation identities");
92
+ const target = subject.entryPoint.replaceAll("\\", "/").endsWith("/pyproject.toml") ? subject.root : subject.entryPoint;
93
+ requireValue(isDeepStrictEqual(report.subject, { root: subject.root, entryPoint: target }) && isDeepStrictEqual(report.selection, facts.selection) &&
94
+ isDeepStrictEqual(report.sourceFactsProducer, facts.producer) && report.sourceFactsPolicy === facts.measurementPolicy, "source provenance mismatch");
95
+ const configuration = { policy: asyncUsagePolicy, selectionFingerprint: facts.selection.configurationFingerprint, parserVersion: facts.producer.parserVersion,
96
+ maxOwners: 100000, maxCalls: 100000, blockingCatalogue: blocking, offloadCatalogue: offload, wrapperCatalogue: [wrapper] };
97
+ requireValue(isDeepStrictEqual(report.configuration, configuration) && report.configurationFingerprint === fingerprint(configuration), "configuration mismatch");
98
+ requireValue(nonempty(report.scope) && typeof report.generatedAtUtc === "string" && Number.isFinite(Date.parse(report.generatedAtUtc)), "missing scope/time");
99
+ requireValue(Array.isArray(report.functions) && Array.isArray(report.diagnostics), "collections required");
100
+ for (const item of report.diagnostics) {
101
+ keys(item, item.file === undefined ? ["code", "message"] : ["code", "message", "file"]);
102
+ requireValue(nonempty(item.code) && typeof item.message === "string" && (item.file === undefined || typeof item.file === "string"), "invalid diagnostic");
103
+ }
104
+ if (report.status === "incomplete") {
105
+ requireValue(report.diagnostics.length > 0 && report.functions.length === 0 && report.summary === null, "incomplete review must withhold rows");
106
+ return;
107
+ }
108
+ requireValue(report.status === "complete" && facts.status === "complete" && !report.diagnostics.length && report.functions.length <= 100000, "status/owner budget mismatch");
109
+ const owners = new Map(facts.functions.map(row => [row.id, row]));
110
+ const bodies = new Map(decomposition.functions.map(row => [row.id, row]));
111
+ const files = new Map(facts.selection.files.map(row => [row.file, row]));
112
+ const used = new Set(), calls = new Set();
113
+ for (const row of report.functions) {
114
+ keys(row, ["id", "file", "member", "line", "population", "sourceSha256", "endLine", "functionKind", "disposition", "status", "reason", "measurement"]);
115
+ const owner = owners.get(row.id), body = bodies.get(row.id);
116
+ requireValue(owner && !used.has(row.id), "unknown or repeated owner");
117
+ used.add(row.id);
118
+ requireValue(row.file === owner.file && row.member === owner.member && row.line === owner.line && row.population === owner.population &&
119
+ row.sourceSha256 === files.get(row.file)?.contentSha256 && integer(row.endLine) && row.endLine >= row.line && row.disposition === "review-only", "owner location/source mismatch");
120
+ if (decomposition.status === "complete")
121
+ requireValue(body && row.functionKind === body.functionKind && row.endLine === body.endLine, "owner differs from decomposition");
122
+ if (["FunctionDef", "Lambda"].includes(row.functionKind)) {
123
+ requireValue(row.status === "excluded" && row.reason === "synchronous-owner" && row.measurement === null, "synchronous owner must be excluded");
124
+ continue;
125
+ }
126
+ requireValue(row.functionKind === "AsyncFunctionDef" && row.status === "observed" && row.reason === null, "async owner required");
127
+ keys(row.measurement, ["kind", "awaitExpressions", "calls"]);
128
+ const measurement = row.measurement;
129
+ requireValue(["coroutine", "async-generator"].includes(measurement.kind) && Array.isArray(measurement.calls), "invalid async measurement");
130
+ requireValue(measurement.awaitExpressions !== null && typeof measurement.awaitExpressions === "object" && !Array.isArray(measurement.awaitExpressions), "await histogram required");
131
+ for (const [key, count] of Object.entries(measurement.awaitExpressions))
132
+ requireValue(["async-body", "deferred-generator-body"].includes(key) && integer(count) && count > 0, "invalid await count");
133
+ for (const call of measurement.calls)
134
+ callEvidence(call, row, calls);
135
+ for (const context of ["async-body", "deferred-generator-body"])
136
+ requireValue(measurement.calls.filter(call => call.executionContext === context && call.directlyAwaited).length <= (measurement.awaitExpressions[context] ?? 0), "direct-await accounting mismatch");
137
+ requireValue(calls.size <= 100000, "call budget exceeded");
138
+ }
139
+ requireValue(used.size === owners.size, "missing selected owners");
140
+ const expected = { ...counts(report.functions), populations: Object.fromEntries([...new Set(report.functions.map(row => row.population))].sort()
141
+ .map(population => [population, counts(report.functions.filter(row => row.population === population))])) };
142
+ requireValue(isDeepStrictEqual(report.summary, expected), "summary accounting mismatch");
143
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ import { analyzePython } from "./python-analyzer.js";
6
+ import { pythonVersion } from "./python-evidence.js";
7
+ import { validateEvidence } from "./evidence-tools.js";
8
+ try {
9
+ const { values } = parseArgs({ options: {
10
+ project: { type: "string" }, python: { type: "string" }, "scorecard-output": { type: "string" },
11
+ "run-id": { type: "string" }, "audit-id": { type: "string" },
12
+ "coverage-json": { type: "string" }, "collection-manifest": { type: "string" },
13
+ include: { type: "string", multiple: true }, exclude: { type: "string", multiple: true },
14
+ "test-path": { type: "string", multiple: true }, "production-path": { type: "string", multiple: true },
15
+ "include-tests": { type: "boolean" }, "include-generated": { type: "boolean" }, "include-support": { type: "boolean" },
16
+ help: { type: "boolean", short: "h" }, version: { type: "boolean" }
17
+ } });
18
+ if (values.help)
19
+ console.log([
20
+ "codemetrics-python — Python 3.12 Method Complexity scoring and unscored decomposition, duplication, and coverage evidence (Node 20+)",
21
+ "--project <directory|pyproject.toml|file.py> --scorecard-output <new-evidence.json>",
22
+ "--python <absolute Python 3.12 interpreter path> (default: py -3.12 on Windows; python3 elsewhere)",
23
+ "--include <pattern> --exclude <pattern> --test-path <pattern> --production-path <pattern> (repeatable)",
24
+ "--include-tests --include-generated --include-support --run-id <uuid> --audit-id <uuid>",
25
+ "--coverage-json <coverage.py JSON> --collection-manifest <optional collection receipt>",
26
+ "Coverage/CRAP is unscored. Missing coverage stays unavailable; unbound imports stay conditional. Provenance mismatches fail.",
27
+ "Emits evidence v3 with one scored dimension for nonempty production implementations. Other dimensions are unavailable. No CSV.",
28
+ "Matching supported policies and selections can compare Method Complexity scores through codemetrics-evidence. Exact whole-body duplication is review-only, with a five-statement minimum. Zero matches does not prove no duplication.",
29
+ "Target code is parsed, never installed or executed. Exit 0: complete (possibly empty); 2: invalid/incomplete."
30
+ ].join("\n"));
31
+ else if (values.version)
32
+ console.log(pythonVersion);
33
+ else {
34
+ if (!values.project || !values["scorecard-output"])
35
+ throw new Error("--project and --scorecard-output are required.");
36
+ const output = path.resolve(values["scorecard-output"]);
37
+ if (path.extname(output) !== ".json" || fs.existsSync(output))
38
+ throw new Error("Evidence output must be a new .json file; existing files are never overwritten.");
39
+ const evidence = analyzePython({ project: values.project, python: values.python, runId: values["run-id"], auditId: values["audit-id"],
40
+ coverageJson: values["coverage-json"], collectionManifest: values["collection-manifest"],
41
+ include: values.include, exclude: values.exclude, testPath: values["test-path"], productionPath: values["production-path"],
42
+ includeTests: values["include-tests"], includeGenerated: values["include-generated"], includeSupport: values["include-support"] });
43
+ validateEvidence(evidence);
44
+ fs.mkdirSync(path.dirname(output), { recursive: true });
45
+ fs.writeFileSync(output, JSON.stringify(evidence, null, 2) + "\n", { flag: "wx" });
46
+ console.log(`Python ${evidence.analysis.status}; ${evidence.population.members} implementation bodies. Evidence: ${output}`);
47
+ process.exitCode = evidence.analysis.status === "complete" ? 0 : 2;
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error(error instanceof Error ? error.message : error);
52
+ process.exitCode = 2;
53
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+ // Explicit opt-in test execution; never called by the scorecard analyzer.
6
+ try {
7
+ const args = process.argv.slice(2);
8
+ const separator = args.indexOf("--");
9
+ const options = separator < 0 ? args : args.slice(0, separator);
10
+ const index = options.indexOf("--python");
11
+ const supplied = index >= 0 ? options[index + 1] : undefined;
12
+ if (supplied && !path.isAbsolute(supplied))
13
+ throw new Error("--python must be an absolute Python 3.12 interpreter path.");
14
+ if (!supplied && !options.includes("--help") && !options.includes("-h"))
15
+ throw new Error("Explicit collection executes tests. Supply --python with an environment containing coverage.py and the project's test dependencies.");
16
+ const script = fileURLToPath(new URL("./python/coverage_collect.py", import.meta.url));
17
+ 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__')";
18
+ const command = supplied ?? (process.platform === "win32" ? "py" : "python3");
19
+ const result = spawnSync(command, [...(!supplied && process.platform === "win32" ? ["-3.12"] : []),
20
+ "-I", "-S", "-B", "-c", bootstrap, script, ...args], { stdio: "inherit", windowsHide: true });
21
+ if (result.error)
22
+ throw result.error;
23
+ process.exitCode = result.status === 0 ? 0 : 2;
24
+ }
25
+ catch (error) {
26
+ console.error(error instanceof Error ? error.message : error);
27
+ process.exitCode = 2;
28
+ }
@@ -0,0 +1,18 @@
1
+ import type { SourceFacts } from "#source-facts/contract.mjs";
2
+ import { type Dimension } from "./evidence.js";
3
+ export declare const pythonComplexityPolicy = "python/method-complexity-worst-40-remainder-60/v1";
4
+ export declare const pythonCalibration = "baseline";
5
+ /** Score each selected production implementation once; other populations remain review evidence. */
6
+ export declare function methodComplexity(facts: SourceFacts | null): {
7
+ status: string;
8
+ scoringPolicy: string;
9
+ measurementPolicy: string;
10
+ comparableAcrossLanguages: boolean;
11
+ selectedFunctions: number | null;
12
+ eligibleFunctions: number | null;
13
+ excludedPopulations: {
14
+ [k: string]: number;
15
+ } | null;
16
+ population: import("#source-facts/population.mjs").PopulationResult | null;
17
+ };
18
+ export declare function scoreMethodComplexity(dimension: Dimension, facts: SourceFacts | null, complete: boolean): Dimension;
@@ -0,0 +1,36 @@
1
+ import { aggregatePopulation, complexityAnchors } from "#source-facts/population.mjs";
2
+ import { scored } from "./evidence.js";
3
+ import { firstMatch } from "./scoring-decision.js";
4
+ export const pythonComplexityPolicy = "python/method-complexity-worst-40-remainder-60/v1";
5
+ export const pythonCalibration = "baseline";
6
+ /** Score each selected production implementation once; other populations remain review evidence. */
7
+ export function methodComplexity(facts) {
8
+ const available = facts?.status === "complete" && facts.capabilities.functionComplexity === "available";
9
+ const selected = available ? facts.functions : [];
10
+ const eligible = selected.filter(item => item.population === "production");
11
+ const population = available ? aggregatePopulation(eligible.map(item => ({ id: item.id, value: item.complexity.value })), complexityAnchors, true, 1) : null;
12
+ return { status: population ? "scored" : "unavailable", scoringPolicy: pythonComplexityPolicy,
13
+ measurementPolicy: "python/owned-function-complexity-v2", comparableAcrossLanguages: false,
14
+ selectedFunctions: available ? selected.length : null, eligibleFunctions: available ? eligible.length : null,
15
+ excludedPopulations: available ? Object.fromEntries(["test", "support", "generated"].map(kind => [kind, selected.filter(item => item.population === kind).length])) : null,
16
+ population };
17
+ }
18
+ export function scoreMethodComplexity(dimension, facts, complete) {
19
+ const details = methodComplexity(complete ? facts : null);
20
+ const { experimentalPreview: _preview, ...base } = dimension;
21
+ const findings = base.findings.map(finding => finding.category === "methodComplexityReview" ? { ...finding,
22
+ observations: { ...finding.observations, scoringDisposition: finding.observations.population === "production" ? "aggregate-policy-input" : "unscored" }
23
+ } : finding);
24
+ const scope = { id: pythonComplexityPolicy, coverage: "partial",
25
+ includes: ["selected-production-function-complexity"],
26
+ excludes: ["test-support-generated-scoring", "decomposition-scoring", "duplication-scoring", "coverage-crap-scoring", "runtime-performance", "semantic-validation"] };
27
+ if (!details.population)
28
+ return { ...base, scope, findings, methodComplexity: details,
29
+ basis: complete ? "Method Complexity unavailable: no selected production implementations. Other measurements are unscored review evidence." : base.basis };
30
+ const decision = firstMatch(pythonComplexityPolicy, { measurementPolicy: facts.measurementPolicy,
31
+ eligibleFunctions: details.eligibleFunctions, excludedPopulations: details.excludedPopulations,
32
+ anchors: complexityAnchors, weighting: "one-worst-40-percent/remainder-mean-60-percent; singleton-100-percent", rounding: "decimal-half-up-one-place" }, [{ id: "methodComplexity", condition: "nonempty selected production implementation population", matched: true,
33
+ score: details.population.score, categories: ["methodComplexityReview"] }]);
34
+ return { ...base, ...scored(decision, "Method Complexity of selected production implementations. Decomposition, duplication and coverage/CRAP are unscored review evidence.", findings, { population: details.population }, finding => finding.category === "methodComplexityReview" && finding.observations.population === "production"),
35
+ scope, methodComplexity: details };
36
+ }
@@ -0,0 +1,63 @@
1
+ import type { SourceFacts } from "#source-facts/contract.mjs";
2
+ import { type Evidence } from "./evidence.js";
3
+ type ObjectValue = Record<string, unknown>;
4
+ interface CoverageRow {
5
+ id: string;
6
+ file: string;
7
+ member: string;
8
+ line: number;
9
+ endLine: number;
10
+ population: string;
11
+ sourceSha256: string;
12
+ complexity: number;
13
+ coverage: {
14
+ status: string;
15
+ reason?: string;
16
+ basis?: string;
17
+ coveredLines?: number;
18
+ executableLines?: number;
19
+ executedLines?: number[];
20
+ missingLines?: number[];
21
+ excludedLines?: number[];
22
+ fraction?: number;
23
+ };
24
+ crapEstimate: {
25
+ value: number;
26
+ disposition: string;
27
+ basis: string;
28
+ formula: string;
29
+ } | null;
30
+ }
31
+ export interface PythonCoverage {
32
+ schemaVersion: number;
33
+ artifactKind: string;
34
+ producer: {
35
+ name: string;
36
+ version: string;
37
+ };
38
+ generatedAtUtc: string;
39
+ runId: string;
40
+ auditId: string;
41
+ status: string;
42
+ policy: string;
43
+ configuration: ObjectValue;
44
+ configurationFingerprint: string;
45
+ subject: {
46
+ root: string;
47
+ entryPoint: string;
48
+ };
49
+ sourceFactsProducer: SourceFacts["producer"];
50
+ selection: SourceFacts["selection"];
51
+ coverageInput: ObjectValue | null;
52
+ collectionProvenance: ObjectValue | null;
53
+ diagnostics: SourceFacts["diagnostics"];
54
+ scope: string;
55
+ functions: CoverageRow[];
56
+ summary: ObjectValue | null;
57
+ }
58
+ /** Offline integrity checks; this does not rerun tests or authenticate unsigned receipts. */
59
+ export declare function validateCoverage(value: unknown, facts: SourceFacts, subject: Evidence["subject"], ids: {
60
+ runId: string;
61
+ auditId: string;
62
+ }): asserts value is PythonCoverage;
63
+ export {};
@@ -0,0 +1,173 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hash } from "./evidence.js";
3
+ const policy = "python/coveragepy-line-crap-review-v1";
4
+ const formula = "CC^2 * (1 - coveredLines / executableLines)^3 + CC";
5
+ function requireValue(value, message) {
6
+ if (!value)
7
+ throw new Error(`Invalid Python coverage evidence: ${message}.`);
8
+ }
9
+ function object(value) {
10
+ requireValue(value !== null && typeof value === "object" && !Array.isArray(value), "expected object");
11
+ }
12
+ function keys(value, fields) {
13
+ object(value);
14
+ requireValue(isDeepStrictEqual(Object.keys(value).sort(), [...fields].sort()), "unexpected or missing fields");
15
+ }
16
+ const text = (value) => typeof value === "string" && value.length > 0;
17
+ const hex = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
18
+ const integer = (value) => Number.isSafeInteger(value) && value >= 0;
19
+ const timestamp = (value) => text(value) && Number.isFinite(Date.parse(value));
20
+ const uuid = (value) => typeof value === "string" && /^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value);
21
+ const relative = (value) => text(value) && !value.includes("\\") && !value.includes(":") && !value.startsWith("/") && value.split("/").every(part => part && part !== "." && part !== "..");
22
+ function sameRoot(value, expected) {
23
+ if (!text(value))
24
+ return false;
25
+ const normalize = (name) => {
26
+ const result = name.replaceAll("\\", "/").replace(/\/$/, "");
27
+ return /^[a-z]:\//i.test(result) ? result.toLowerCase() : result;
28
+ };
29
+ return normalize(value) === normalize(expected);
30
+ }
31
+ /** Python's sorted, ASCII JSON convention used for source snapshot digests. */
32
+ function canonical(value) {
33
+ if (Array.isArray(value))
34
+ return `[${value.map(canonical).join(",")}]`;
35
+ if (value !== null && typeof value === "object")
36
+ return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
37
+ .map(([key, item]) => `${canonical(key)}:${canonical(item)}`).join(",")}}`;
38
+ return JSON.stringify(value).replace(/[\u007f-\uffff]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
39
+ }
40
+ function collection(value, input, facts, subject) {
41
+ keys(value, ["path", "sha256", "collectionId", "policy", "startedAtUtc", "finishedAtUtc", "sourceBinding", "freshness", "command", "receipt", "limitations"]);
42
+ const bound = value;
43
+ requireValue(input && text(bound.path) && hex(bound.sha256) && uuid(bound.collectionId) && bound.policy === "python/coverage-collection-v1" &&
44
+ bound.sourceBinding === "matched-pre-post-snapshots" && bound.freshness === "source-matched" && text(bound.limitations), "invalid collection binding");
45
+ keys(bound.receipt, ["schemaVersion", "policy", "collectionId", "startedAtUtc", "finishedAtUtc", "root", "status", "configuration", "sourceSelection",
46
+ "beforeSourceSha256", "afterSourceSha256", "sourceFactsPolicy", "sourceFactsProducer", "reportSha256", "command", "runtime", "testExitCode", "diagnostics"]);
47
+ const receipt = bound.receipt;
48
+ requireValue(receipt.schemaVersion === 1 && receipt.status === "complete" && receipt.testExitCode === 0 &&
49
+ sameRoot(receipt.root, subject.root) && receipt.reportSha256 === input.sha256 && input.branchDataPresent === true &&
50
+ isDeepStrictEqual(receipt.diagnostics, []), "failed or mismatched collection receipt");
51
+ for (const key of ["policy", "collectionId", "startedAtUtc", "finishedAtUtc", "command"])
52
+ requireValue(isDeepStrictEqual(bound[key], receipt[key]), "collection summary disagrees with receipt");
53
+ requireValue(timestamp(receipt.startedAtUtc) && timestamp(receipt.finishedAtUtc) &&
54
+ /(?:Z|[+-]\d\d:\d\d)$/.test(receipt.startedAtUtc) && /(?:Z|[+-]\d\d:\d\d)$/.test(receipt.finishedAtUtc) &&
55
+ Date.parse(receipt.finishedAtUtc) >= Date.parse(receipt.startedAtUtc), "invalid collection times");
56
+ requireValue(Array.isArray(receipt.command) && receipt.command.length > 0 && receipt.command.every(text), "missing collection command");
57
+ requireValue(isDeepStrictEqual(receipt.configuration, { branch: true, configFile: false, processScope: "single-process", freshBytecodeCache: true }), "collection configuration mismatch");
58
+ keys(receipt.runtime, ["coverageVersion", "pythonVersion", "testExitCode"]);
59
+ const runtime = receipt.runtime;
60
+ requireValue(runtime.coverageVersion === input.version && text(runtime.pythonVersion) && /^3\.12\./.test(runtime.pythonVersion) && runtime.testExitCode === 0, "collection runtime mismatch");
61
+ const selected = facts.selection;
62
+ const snapshot = { policy: selected.policy, configurationFingerprint: selected.configurationFingerprint, options: selected.options,
63
+ files: selected.files.filter(row => row.disposition === "analyzed"), excludedFunctions: selected.excludedFunctions };
64
+ const digest = hash(canonical(snapshot));
65
+ requireValue(receipt.beforeSourceSha256 === digest && receipt.afterSourceSha256 === digest && isDeepStrictEqual(receipt.sourceSelection, snapshot) &&
66
+ receipt.sourceFactsPolicy === facts.measurementPolicy && isDeepStrictEqual(receipt.sourceFactsProducer, facts.producer), "collection source snapshot mismatch");
67
+ }
68
+ function inputs(report, facts, subject) {
69
+ const input = report.coverageInput;
70
+ if (input !== null) {
71
+ keys(input, ["path", "sha256", "producer", "version", "format", "reportedAt", "branchDataPresent", "sourceBinding", "freshness"]);
72
+ requireValue(text(input.path) && hex(input.sha256) && input.producer === "coverage.py" && text(input.version) &&
73
+ [2, 3].includes(input.format) && typeof input.reportedAt === "string" && typeof input.branchDataPresent === "boolean", "invalid coverage input");
74
+ // Failed reviews may retain verified receipt diagnostics, but never promote input status.
75
+ const bound = report.status === "complete" && report.collectionProvenance !== null;
76
+ requireValue(input.sourceBinding === (bound ? "matched-pre-post-snapshots" : "unverified") &&
77
+ input.freshness === (bound ? "source-matched" : "unverified"), "unjustified coverage freshness");
78
+ }
79
+ if (report.collectionProvenance !== null)
80
+ collection(report.collectionProvenance, input, facts, subject);
81
+ }
82
+ function lines(value, row) {
83
+ requireValue(Array.isArray(value) && value.every((line, index) => integer(line) && line > row.line && line <= row.endLine &&
84
+ (index === 0 || line > value[index - 1])), "invalid body line attribution");
85
+ }
86
+ function observations(row, input, bound) {
87
+ const coverage = row.coverage;
88
+ if (coverage.status === "unavailable") {
89
+ keys(coverage, ["status", "reason", ...(coverage.excludedLines === undefined ? [] : ["excludedLines"])]);
90
+ requireValue(row.crapEstimate === null && (input === null ? coverage.reason === "coverage-not-supplied" :
91
+ ["lambda-line-attribution", "body-shares-definition-line", "body-shares-docstring-line", "nested-definition", "no-reportable-body-lines", "file-not-in-report"].includes(coverage.reason)), "invalid unavailable reason");
92
+ if (coverage.excludedLines !== undefined) {
93
+ lines(coverage.excludedLines, row);
94
+ requireValue(coverage.reason === "no-reportable-body-lines", "unexpected excluded lines");
95
+ }
96
+ return;
97
+ }
98
+ keys(coverage, ["status", "basis", "coveredLines", "executableLines", "executedLines", "missingLines", "excludedLines", "fraction"]);
99
+ requireValue(input !== null && coverage.status === (bound ? "reported-source-matched" : "reported-unverified") && coverage.basis === "executable-line", "unjustified coverage status");
100
+ lines(coverage.executedLines, row);
101
+ lines(coverage.missingLines, row);
102
+ lines(coverage.excludedLines, row);
103
+ const all = [...coverage.executedLines, ...coverage.missingLines, ...coverage.excludedLines];
104
+ const covered = coverage.executedLines.length, total = covered + coverage.missingLines.length;
105
+ requireValue(new Set(all).size === all.length && total > 0 && coverage.coveredLines === covered && coverage.executableLines === total &&
106
+ coverage.fraction === covered / total, "invalid coverage arithmetic");
107
+ keys(row.crapEstimate, ["value", "disposition", "basis", "formula"]);
108
+ const estimate = row.crapEstimate;
109
+ const expected = row.complexity ** 2 * ((total - covered) / total) ** 3 + row.complexity;
110
+ requireValue(Number.isFinite(estimate.value) && Math.abs(estimate.value - expected) <= 1e-12 * Math.max(1, expected) &&
111
+ estimate.disposition === (bound ? "review-only" : "conditional-review-only") && estimate.basis === "line-coverage-proxy" && estimate.formula === formula, "invalid CRAP estimate");
112
+ }
113
+ /** Offline integrity checks; this does not rerun tests or authenticate unsigned receipts. */
114
+ export function validateCoverage(value, facts, subject, ids) {
115
+ keys(value, ["schemaVersion", "artifactKind", "producer", "generatedAtUtc", "runId", "auditId", "policy", "configuration", "configurationFingerprint", "status",
116
+ "subject", "sourceFactsProducer", "selection", "coverageInput", "collectionProvenance", "diagnostics", "scope", "functions", "summary"]);
117
+ const report = value;
118
+ requireValue(report.schemaVersion === 2 && report.artifactKind === "experimental-python-coverage-review" && report.policy === policy &&
119
+ isDeepStrictEqual(report.producer, { name: "codemetrics-ai/python-coverage-review", version: "0.3.0-experimental" }), "unsupported coverage producer or policy");
120
+ requireValue(report.runId === ids.runId && report.auditId === ids.auditId, "stale coverage identities");
121
+ const target = subject.entryPoint.replaceAll("\\", "/").endsWith("/pyproject.toml") ? subject.root : subject.entryPoint;
122
+ requireValue(isDeepStrictEqual(report.subject, { root: subject.root, entryPoint: target }) && isDeepStrictEqual(report.selection, facts.selection) &&
123
+ isDeepStrictEqual(report.sourceFactsProducer, facts.producer), "source selection/provenance mismatch");
124
+ const configuration = { policy, selectionFingerprint: facts.selection.configurationFingerprint, complexityPolicy: facts.measurementPolicy,
125
+ parserVersion: facts.producer.parserVersion, maxReportBytes: 33554432, maxOwners: 100000 };
126
+ requireValue(isDeepStrictEqual(report.configuration, configuration) && report.configurationFingerprint === hash(canonical(configuration)), "configuration mismatch");
127
+ requireValue(timestamp(report.generatedAtUtc) && text(report.scope) && Array.isArray(report.diagnostics) && Array.isArray(report.functions), "missing report metadata");
128
+ for (const item of report.diagnostics) {
129
+ keys(item, ["code", "message", ...(item.file === undefined ? [] : ["file"])]);
130
+ requireValue(text(item.code) && typeof item.message === "string" && (item.file === undefined || text(item.file)), "invalid diagnostic");
131
+ }
132
+ inputs(report, facts, subject);
133
+ if (report.status === "incomplete") {
134
+ requireValue(report.functions.length === 0 && report.summary === null && report.diagnostics.length > 0, "incomplete review must withhold estimates");
135
+ return;
136
+ }
137
+ requireValue(report.status === "complete" && facts.status === "complete" && report.diagnostics.length === 0, "status mismatch");
138
+ const owners = new Map(facts.functions.map(row => [row.id, row])), used = new Set(), usedLines = new Set();
139
+ const files = new Map(facts.selection.files.map(row => [row.file, row]));
140
+ const reasons = {};
141
+ let estimates = 0;
142
+ for (const row of report.functions) {
143
+ keys(row, ["id", "file", "member", "line", "population", "endLine", "complexity", "coverage", "crapEstimate", "sourceSha256"]);
144
+ const owner = owners.get(row.id);
145
+ requireValue(owner && !used.has(row.id) && row.file === owner.file && row.member === owner.member && row.line === owner.line &&
146
+ row.population === owner.population && row.complexity === owner.complexity?.value && row.sourceSha256 === files.get(row.file)?.contentSha256 &&
147
+ integer(row.endLine) && row.endLine >= row.line, "unknown, repeated, or mismatched function");
148
+ used.add(row.id);
149
+ object(row.coverage);
150
+ observations(row, report.coverageInput, report.collectionProvenance !== null);
151
+ if (row.crapEstimate === null)
152
+ reasons[row.coverage.reason] = (reasons[row.coverage.reason] ?? 0) + 1;
153
+ else {
154
+ estimates++;
155
+ for (const line of [...row.coverage.executedLines, ...row.coverage.missingLines]) {
156
+ const key = `${row.file}:${line}`;
157
+ requireValue(!usedLines.has(key), "coverage line credited to multiple owners");
158
+ usedLines.add(key);
159
+ }
160
+ }
161
+ }
162
+ requireValue(used.size === owners.size, "incomplete function population");
163
+ keys(report.summary, ["selectedFunctions", "conditionalEstimates", "sourceBoundEstimates", "unavailableFunctions", "unavailableReasons", "matchedFiles", "unmatchedReportFiles"]);
164
+ const summary = report.summary;
165
+ requireValue(summary.selectedFunctions === owners.size && summary.conditionalEstimates === (report.collectionProvenance ? 0 : estimates) &&
166
+ summary.sourceBoundEstimates === (report.collectionProvenance ? estimates : 0) && summary.unavailableFunctions === owners.size - estimates &&
167
+ isDeepStrictEqual(summary.unavailableReasons, reasons), "summary accounting mismatch");
168
+ requireValue(integer(summary.matchedFiles) && summary.matchedFiles <= facts.selection.files.filter(file => file.disposition === "analyzed").length &&
169
+ Array.isArray(summary.unmatchedReportFiles) && summary.unmatchedReportFiles.every(file => relative(file) && files.get(file)?.disposition !== "analyzed") &&
170
+ new Set(summary.unmatchedReportFiles).size === summary.unmatchedReportFiles.length, "invalid file accounting");
171
+ if (report.coverageInput === null)
172
+ requireValue(summary.matchedFiles === 0 && summary.unmatchedReportFiles.length === 0, "missing coverage cannot contain file observations");
173
+ }
@@ -0,0 +1,56 @@
1
+ import type { SourceFacts } from "#source-facts/contract.mjs";
2
+ import { type Evidence } from "./evidence.js";
3
+ export declare const decompositionPolicy = "python/owned-statements-block-nesting-v1";
4
+ interface Measurement {
5
+ ownedStatements: number;
6
+ maxBlockNesting: number;
7
+ statementKinds: Record<string, number>;
8
+ excludedSyntax: Record<string, number>;
9
+ }
10
+ interface Owner {
11
+ id: string;
12
+ file: string;
13
+ member: string;
14
+ line: number;
15
+ endLine: number;
16
+ population: string;
17
+ sourceSha256: string;
18
+ functionKind: string;
19
+ complexity: number;
20
+ disposition: string;
21
+ status: string;
22
+ measurement: Measurement | null;
23
+ reason?: string;
24
+ }
25
+ export interface PythonDecomposition {
26
+ schemaVersion: number;
27
+ artifactKind: string;
28
+ producer: {
29
+ name: string;
30
+ version: string;
31
+ };
32
+ generatedAtUtc: string;
33
+ runId: string;
34
+ auditId: string;
35
+ status: string;
36
+ policy: string;
37
+ configuration: Record<string, string | number>;
38
+ configurationFingerprint: string;
39
+ subject: {
40
+ root: string;
41
+ entryPoint: string;
42
+ };
43
+ sourceFactsProducer: SourceFacts["producer"];
44
+ sourceFactsPolicy: string;
45
+ selection: SourceFacts["selection"];
46
+ scope: string;
47
+ diagnostics: SourceFacts["diagnostics"];
48
+ functions: Owner[];
49
+ summary: unknown;
50
+ }
51
+ /** Validate provenance and internal accounting; AST measurements remain the producer's observations. */
52
+ export declare function validateDecomposition(value: unknown, facts: SourceFacts, subject: Evidence["subject"], ids: {
53
+ runId: string;
54
+ auditId: string;
55
+ }): asserts value is PythonDecomposition;
56
+ export {};