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,255 @@
1
+ import { validateAsyncHelpers, validateAsyncScoring, scorePythonAsync } from "./python-async-scoring.js";
2
+ import { validateAsyncAdapters } from "./python-async-adapters.js";
3
+ import { validateAsyncContext } from "./python-async-context.js";
4
+ import { validateAsyncUsage, asyncUsagePolicy } from "./python-async-usage.js";
5
+ import { isDeepStrictEqual } from "node:util";
6
+ import { complexityPopulation } from "#source-facts/population.mjs";
7
+ import { validateSourceFacts } from "#source-facts/contract.mjs";
8
+ import { dimensionKeys } from "./scorecard-contract.js";
9
+ import { hash } from "./evidence.js";
10
+ import { duplicationFindings, validateDuplication } from "./python-duplication.js";
11
+ import { validateCoverage } from "./python-coverage.js";
12
+ import { validateDecomposition } from "./python-decomposition.js";
13
+ import { validateErrorHandling, errorHandlingPolicy } from "./python-error-handling.js";
14
+ import { validateErrorContext } from "./python-error-context.js";
15
+ import { validateOperations } from "./python-error-operations.js";
16
+ import { validateTransfers } from "./python-error-transfers.js";
17
+ import { scoreMethodComplexity, pythonCalibration } from "./python-complexity.js";
18
+ export const pythonVersion = "0.15.0";
19
+ export const pythonRuleset = "python-2026-10-28-async-scoring-v1";
20
+ const currentProfile = { version: pythonVersion, ruleset: pythonRuleset, decomposition: true, scored: true, errorHandling: true, context: true, transfers: true, operations: true, asyncUsage: true, asyncContext: true, asyncAdapters: true, asyncScoring: true };
21
+ const adapterProfile = { ...currentProfile, version: "0.14.0", ruleset: "python-2026-10-24-async-adapters-review-v1", asyncScoring: false };
22
+ const asyncContextProfile = { ...adapterProfile, version: "0.13.0", ruleset: "python-2026-10-18-async-context-review-v1", asyncAdapters: false, asyncScoring: false };
23
+ const asyncProfile = { ...asyncContextProfile, version: "0.12.0", ruleset: "python-2026-10-16-async-usage-review-v1", asyncContext: false, asyncAdapters: false, asyncScoring: false };
24
+ const operationProfile = { version: "0.11.0", ruleset: "python-2026-10-15-exception-operations-review-v1", decomposition: true, scored: true, errorHandling: true, context: true, transfers: true, operations: true, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
25
+ const transferProfile = { version: "0.10.0", ruleset: "python-2026-10-15-exception-transfers-review-v1", decomposition: true, scored: true, errorHandling: true, context: true, transfers: true, operations: false, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
26
+ const contextProfile = { version: "0.9.0", ruleset: "python-2026-10-15-exception-context-review-v1", decomposition: true, scored: true, errorHandling: true, context: true, transfers: false, operations: false, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
27
+ const handlerProfile = { version: "0.8.0", ruleset: "python-2026-10-15-error-handling-review-v1", decomposition: true, scored: true, errorHandling: true, context: false, transfers: false, operations: false, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
28
+ const complexityProfile = { version: "0.7.0", ruleset: "python-2026-10-13-method-complexity-v1", decomposition: true, scored: true, errorHandling: false, context: false, transfers: false, operations: false, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
29
+ const decompositionProfile = { version: "0.6.0-experimental", ruleset: "python-2026-10-13-decomposition-preview", decomposition: true, scored: false, errorHandling: false, context: false, transfers: false, operations: false, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
30
+ const coverageProfile = { version: "0.5.0-experimental", ruleset: "python-2026-10-12-coverage-preview", decomposition: false, scored: false, errorHandling: false, context: false, transfers: false, operations: false, asyncUsage: false, asyncContext: false, asyncAdapters: false, asyncScoring: false };
31
+ const previewPolicy = "experimental/shared-complexity-worst-40-remainder-60/v1";
32
+ function complexityDetails(facts) {
33
+ const population = facts ? complexityPopulation(facts) : null;
34
+ return { status: population ? "experimental-preview" : "unavailable", scoringPolicy: previewPolicy,
35
+ comparableAcrossLanguages: false, population };
36
+ }
37
+ function reviewFindings(facts) {
38
+ if (facts?.status !== "complete")
39
+ return [];
40
+ return facts.functions.filter(item => item.complexity && item.complexity.value >= 10).map(item => ({
41
+ category: "methodComplexityReview", ruleId: "python/method-complexity/review-v1",
42
+ fingerprint: hash(`${facts.measurementPolicy}:${item.id}`), severity: "info", confidence: "high",
43
+ file: item.file, line: item.line, member: item.member,
44
+ message: `Measured CC ${item.complexity.value}; inspect control flow and required behavior before recommending decomposition.`,
45
+ observations: { functionId: item.id, complexity: item.complexity, population: item.population,
46
+ measurementPolicy: facts.measurementPolicy, reviewThreshold: 10, scoringDisposition: "unscored" }
47
+ }));
48
+ }
49
+ function measurements(facts) {
50
+ const files = facts?.selection?.files ?? [];
51
+ return { filters: { totalUnits: files.length, analyzedUnits: files.filter(item => item.disposition === "analyzed").length,
52
+ skipped: files.filter(item => item.disposition === "excluded").map(item => ({ name: item.file, reason: item.reason })) },
53
+ population: { types: 0, members: facts?.functions.length ?? 0 } };
54
+ }
55
+ export function pythonEvidence(facts, subject, ids, error, duplication = null, coverage = null, decomposition = null, errorHandling = null, context = null, transfers = null, operations = null, asyncReview = null, asyncContext = null, asyncAdapters = null, asyncHelpers = null, asyncScoring = null) {
56
+ return buildEvidence(currentProfile, facts, subject, ids, error, duplication, coverage, decomposition, errorHandling, context, transfers, operations, asyncReview, asyncContext, asyncAdapters, asyncHelpers, asyncScoring);
57
+ }
58
+ function buildEvidence(profile, facts, subject, ids, error, duplication, coverage, decomposition, errorHandling, context, transfers, operations, asyncReview, asyncContext, asyncAdapters, asyncHelpers, asyncScoring) {
59
+ if (facts)
60
+ validateSourceFacts(facts);
61
+ if (facts && duplication)
62
+ validateDuplication(duplication, facts, subject, ids);
63
+ if (facts && coverage)
64
+ validateCoverage(coverage, facts, subject, ids);
65
+ if (facts && decomposition)
66
+ validateDecomposition(decomposition, facts, subject, ids);
67
+ if (facts && errorHandling)
68
+ validateErrorHandling(errorHandling, facts, subject, ids);
69
+ if (context && errorHandling)
70
+ validateErrorContext(context, errorHandling);
71
+ if (transfers && context)
72
+ validateTransfers(transfers, context);
73
+ if (operations && transfers)
74
+ validateOperations(operations, transfers);
75
+ if (asyncReview && facts && decomposition)
76
+ validateAsyncUsage(asyncReview, facts, subject, ids, decomposition);
77
+ if (asyncContext && asyncReview)
78
+ validateAsyncContext(asyncContext, asyncReview);
79
+ if (asyncAdapters && asyncReview && asyncContext)
80
+ validateAsyncAdapters(asyncAdapters, asyncReview, asyncContext);
81
+ if (asyncHelpers && asyncReview && asyncContext && asyncAdapters)
82
+ validateAsyncHelpers(asyncHelpers, asyncReview, asyncContext, asyncAdapters);
83
+ if (asyncScoring && asyncHelpers && asyncReview)
84
+ validateAsyncScoring(asyncScoring, asyncHelpers, asyncReview);
85
+ const complete = facts?.status === "complete" && duplication?.status === "complete" && coverage?.status === "complete" && (!profile.decomposition || decomposition?.status === "complete") && (!profile.errorHandling || errorHandling?.status === "complete") && (!profile.context || context?.status === "complete") && (!profile.transfers || transfers?.status === "complete") && (!profile.operations || operations?.status === "complete") && (!profile.asyncUsage || asyncReview?.status === "complete") && (!profile.asyncContext || asyncContext?.status === "complete") && (!profile.asyncAdapters || asyncAdapters?.status === "complete") && (!profile.asyncScoring || asyncHelpers?.status === "complete" && asyncScoring?.status === "complete") && !error;
86
+ const dimensions = Object.fromEntries(dimensionKeys.map(key => [key, {
87
+ status: "skipped", basis: profile.scored ? "Not measured by this Python analyzer." : "Not measured by the experimental Python analyzer.", findings: [],
88
+ scope: { id: `python/${key}/unsupported`, coverage: "unsupported", includes: [], excludes: [key] }
89
+ }]));
90
+ dimensions.codeQuality = {
91
+ status: complete ? "skipped" : "failed",
92
+ basis: complete ? profile.decomposition
93
+ ? "Complexity, owned statements, block nesting, duplication and optional coverage/CRAP review evidence; all unscored. Missing coverage and lambda decomposition are unavailable."
94
+ : "Complexity, duplication and optional coverage/CRAP review evidence; all unscored. Complexity preview is experimental. Missing coverage is unavailable."
95
+ : "Python analysis incomplete; partial measurements are diagnostic only.",
96
+ scope: { id: "python/owned-function-complexity-v2", coverage: "partial", includes: ["selected-function-complexity", "exact-function-body-duplication", "optional-line-coverage-crap-review", ...(profile.decomposition ? ["owned-function-statements", "function-block-nesting"] : [])],
97
+ excludes: ["calibrated-dimension-score", ...(profile.decomposition ? [] : ["decomposition"]), "runtime-performance", "semantic-validation"] },
98
+ findings: complete ? [...reviewFindings(facts), ...duplicationFindings(duplication)] : [],
99
+ duplication, coverageReview: coverage, ...(profile.decomposition ? { decomposition } : {}),
100
+ sourceFacts: facts, experimentalPreview: complete ? complexityDetails(facts) : complexityDetails(null),
101
+ populationNotes: "Units are observed Python files; members are selected implementation bodies. Types are not measured (the required types field is a placeholder zero). Pruned directories do not enumerate unseen files."
102
+ };
103
+ if (profile.scored)
104
+ dimensions.codeQuality = scoreMethodComplexity(dimensions.codeQuality, facts, complete);
105
+ if (profile.errorHandling)
106
+ dimensions.errorHandling = {
107
+ status: complete ? "skipped" : "failed", basis: complete
108
+ ? "Exception-handler syntax and local documentation are review evidence only; no handling-quality score."
109
+ : "Python analysis incomplete; handler observations are diagnostic only.",
110
+ scope: { id: errorHandlingPolicy, coverage: "partial", includes: ["selected-exception-suites", "module-and-class-body-handlers", "local-no-op-documentation", ...(profile.context ? ["bounded-lexical-context", "conservative-handler-patterns"] : []), ...(profile.transfers ? ["owned-raise-cause-syntax", "conservative-raise-patterns"] : []), ...(profile.operations ? ["owned-call-return-syntax", "unresolved-suite-inventory"] : [])],
111
+ excludes: ["calibrated-dimension-score", "resolved-exception-types", "business-correctness", "context-manager-suppression", "path-completeness"] },
112
+ findings: [], review: errorHandling, ...(profile.context ? { contextReview: context } : {}), ...(profile.transfers ? { transferReview: transfers } : {}), ...(profile.operations ? { operationReview: operations } : {})
113
+ };
114
+ if (profile.asyncUsage)
115
+ dimensions.performanceAsync = {
116
+ status: complete ? "skipped" : "failed", basis: complete
117
+ ? "Async-owned calls, import-matched blocking candidates, and thread-helper syntax are unscored review evidence. Unresolved calls are not clean-async evidence."
118
+ : "Python analysis incomplete; async observations are diagnostic only.",
119
+ scope: { id: asyncUsagePolicy, coverage: "partial", includes: ["selected-async-bodies", "import-spelling-matches", "thread-helper-syntax", "separate-deferred-generator-observations", ...(profile.asyncContext ? ["unresolved-call-shape-inventory", "bounded-local-source-links", "stored-wrapper-syntax-links"] : []), ...(profile.asyncAdapters ? ["partial-construction-context", "bounded-partial-references", "sync-to-async-decorator-links"] : [])],
120
+ excludes: ["calibrated-dimension-score", "synchronous-owners", "runtime-performance", "resolved-receiver-types", "offloading-success", "complete-blocking-detection"] },
121
+ findings: [], review: asyncReview, ...(profile.asyncContext ? { contextReview: asyncContext } : {}), ...(profile.asyncAdapters ? { adapterReview: asyncAdapters } : {}), ...(profile.asyncScoring ? { helperReview: asyncHelpers, asyncScoring } : {})
122
+ };
123
+ if (profile.asyncScoring)
124
+ dimensions.performanceAsync = scorePythonAsync(dimensions.performanceAsync, asyncScoring, complete);
125
+ return { schemaVersion: 3, generatedAtUtc: new Date().toISOString(),
126
+ tool: { name: "CodeMetrics.AI.Python", version: profile.version, ecosystem: "python" }, subject,
127
+ ...measurements(facts), dimensions,
128
+ analysis: { ...ids, status: complete ? "complete" : "incomplete", ruleset: profile.ruleset, calibration: profile.scored ? pythonCalibration : "uncalibrated",
129
+ configurationFingerprint: hash(JSON.stringify([profile.ruleset, facts?.producer, facts?.measurementPolicy, facts?.selection?.configurationFingerprint, duplication?.configurationFingerprint, coverage?.configurationFingerprint, ...(profile.decomposition ? [decomposition?.configurationFingerprint] : []), ...(profile.errorHandling ? [errorHandling?.configurationFingerprint] : []), ...(profile.context ? [context?.configurationFingerprint] : []), ...(profile.transfers ? [transfers?.configurationFingerprint] : []), ...(profile.operations ? [operations?.configurationFingerprint] : []), ...(profile.asyncUsage ? [asyncReview?.configurationFingerprint] : []), ...(profile.asyncContext ? [asyncContext?.configurationFingerprint] : []), ...(profile.asyncAdapters ? [asyncAdapters?.configurationFingerprint] : []), ...(profile.asyncScoring ? [asyncHelpers?.configurationFingerprint, asyncScoring?.configurationFingerprint] : [])])),
130
+ diagnostics: [...(facts?.diagnostics ?? []).map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
131
+ ...(duplication?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
132
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
133
+ ...(coverage?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
134
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
135
+ ...(decomposition?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
136
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
137
+ ...(errorHandling?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
138
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
139
+ ...(context?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
140
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
141
+ ...(transfers?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
142
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
143
+ ...(operations?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
144
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
145
+ ...(asyncReview?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
146
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
147
+ ...(asyncContext?.diagnostics ?? []).filter(item => !facts?.diagnostics.some(other => isDeepStrictEqual(item, other)))
148
+ .map(item => ({ kind: item.code, message: item.message, ...(item.file ? { project: item.file } : {}) })),
149
+ ...(asyncAdapters?.diagnostics ?? []).map(item => ({ kind: item.code, message: item.message })),
150
+ ...(asyncHelpers?.diagnostics ?? []).map(item => ({ kind: item.code, message: item.message })),
151
+ ...(asyncScoring?.diagnostics ?? []).map(item => ({ kind: item.code, message: item.message })),
152
+ ...(error ? [{ kind: "pythonExecution", message: error }] : [])], suppressions: [] }
153
+ };
154
+ }
155
+ /** Validate supported scoring and historical review contracts within the shared v3 envelope. */
156
+ export function validatePythonEvidence(evidence) {
157
+ if (evidence.tool.ecosystem !== "python")
158
+ return;
159
+ const profile = [currentProfile, adapterProfile, asyncContextProfile, asyncProfile, operationProfile, transferProfile, contextProfile, handlerProfile, complexityProfile, decompositionProfile, coverageProfile].find(item => item.ruleset === evidence.analysis.ruleset);
160
+ if (!profile) {
161
+ if ([pythonVersion, adapterProfile.version, asyncContextProfile.version, asyncProfile.version, operationProfile.version, transferProfile.version, contextProfile.version, handlerProfile.version, complexityProfile.version, decompositionProfile.version, coverageProfile.version].includes(evidence.tool.version) || evidence.dimensions.codeQuality.coverageReview !== undefined || evidence.dimensions.codeQuality.decomposition !== undefined ||
162
+ evidence.dimensions.performanceAsync.helperReview !== undefined || evidence.dimensions.performanceAsync.asyncScoring !== undefined || evidence.dimensions.performanceAsync.adapterReview !== undefined || evidence.dimensions.performanceAsync.contextReview !== undefined || evidence.dimensions.performanceAsync.review !== undefined || evidence.dimensions.errorHandling.operationReview !== undefined || evidence.dimensions.errorHandling.transferReview !== undefined || evidence.dimensions.errorHandling.contextReview !== undefined || evidence.dimensions.errorHandling.review !== undefined || evidence.dimensions.codeQuality.methodComplexity !== undefined || Object.values(evidence.dimensions).some(item => item.status === "scored"))
163
+ throw new Error("Python review evidence requires its recorded supported ruleset.");
164
+ return; // Historical producers without these extensions retain their original contract.
165
+ }
166
+ if (!profile.asyncScoring && (Object.hasOwn(evidence.dimensions.performanceAsync, "helperReview") || Object.hasOwn(evidence.dimensions.performanceAsync, "asyncScoring")))
167
+ throw new Error("Historical Python evidence cannot contain async scoring measurements.");
168
+ if (!profile.asyncAdapters && Object.hasOwn(evidence.dimensions.performanceAsync, "adapterReview"))
169
+ throw new Error("Historical Python evidence cannot contain callback adapter measurements.");
170
+ if (!profile.asyncContext && Object.hasOwn(evidence.dimensions.performanceAsync, "contextReview"))
171
+ throw new Error("Historical Python evidence cannot contain async context measurements.");
172
+ if (!profile.asyncUsage && Object.hasOwn(evidence.dimensions.performanceAsync, "review"))
173
+ throw new Error("Historical Python evidence cannot contain async measurements.");
174
+ if (!profile.operations && Object.hasOwn(evidence.dimensions.errorHandling, "operationReview"))
175
+ throw new Error("Historical Python evidence cannot contain call/return measurements.");
176
+ if (!profile.transfers && Object.hasOwn(evidence.dimensions.errorHandling, "transferReview"))
177
+ throw new Error("Historical Python evidence cannot contain raise/chaining measurements.");
178
+ if (!profile.context && Object.hasOwn(evidence.dimensions.errorHandling, "contextReview"))
179
+ throw new Error("Historical Python evidence cannot contain exception context measurements.");
180
+ if (!profile.errorHandling && Object.hasOwn(evidence.dimensions.errorHandling, "review"))
181
+ throw new Error("Historical Python evidence cannot contain handler measurements.");
182
+ if (!profile.decomposition && Object.hasOwn(evidence.dimensions.codeQuality, "decomposition"))
183
+ throw new Error("Historical Python coverage evidence cannot contain decomposition measurements.");
184
+ if (!evidence.analysis.runId || !evidence.analysis.auditId || evidence.analysis.calibration !== (profile.scored ? pythonCalibration : "uncalibrated") ||
185
+ evidence.tool.version !== profile.version || evidence.tool.name !== "CodeMetrics.AI.Python")
186
+ throw new Error("Python evidence requires invocation IDs and matching versioned provenance.");
187
+ if (Object.entries(evidence.dimensions).some(([key, item]) => item.status === "scored" && (!profile.scored || key !== "codeQuality" && !(profile.asyncScoring && key === "performanceAsync"))))
188
+ throw new Error("Only supported Python Method Complexity and Async/Blocking Usage can be scored.");
189
+ const facts = evidence.dimensions.codeQuality.sourceFacts;
190
+ const duplication = evidence.dimensions.codeQuality.duplication;
191
+ const coverage = evidence.dimensions.codeQuality.coverageReview;
192
+ const decomposition = profile.decomposition ? evidence.dimensions.codeQuality.decomposition : null;
193
+ const errorHandling = profile.errorHandling ? evidence.dimensions.errorHandling.review : null;
194
+ const context = profile.context ? evidence.dimensions.errorHandling.contextReview : null;
195
+ const transfers = profile.transfers ? evidence.dimensions.errorHandling.transferReview : null;
196
+ const operations = profile.operations ? evidence.dimensions.errorHandling.operationReview : null;
197
+ const asyncReview = profile.asyncUsage ? evidence.dimensions.performanceAsync.review : null;
198
+ if (facts === null && (duplication !== null || coverage !== null || decomposition !== null || errorHandling !== null || context !== null || transfers !== null || operations !== null || asyncReview !== null))
199
+ throw new Error("Python measurements require source facts.");
200
+ const asyncAdapters = profile.asyncAdapters ? evidence.dimensions.performanceAsync.adapterReview : null;
201
+ const asyncContext = profile.asyncContext ? evidence.dimensions.performanceAsync.contextReview : null;
202
+ const asyncHelpers = profile.asyncScoring ? evidence.dimensions.performanceAsync.helperReview : null;
203
+ const asyncScoring = profile.asyncScoring ? evidence.dimensions.performanceAsync.asyncScoring : null;
204
+ if (facts === null && (asyncHelpers !== null || asyncScoring !== null))
205
+ throw new Error("Python async scoring requires source facts.");
206
+ if (facts === null && (asyncContext !== null || asyncAdapters !== null))
207
+ throw new Error("Python async context and adapters require source facts.");
208
+ if (facts !== null) {
209
+ validateSourceFacts(facts);
210
+ if (facts.language !== "python" || facts.schemaVersion !== 2 || facts.producer.version !== "0.4.0-experimental" ||
211
+ facts.producer.name !== "codemetrics-ai/python-source-facts" || facts.producer.parser !== "python-ast" ||
212
+ facts.measurementPolicy !== "python/owned-function-complexity-v2" || facts.selection?.policy !== "python/source-selection-v1" ||
213
+ (facts.status === "complete" && !/^3\.12\./.test(facts.producer.parserVersion)))
214
+ throw new Error("Unexpected Python source-facts producer or version.");
215
+ validateDuplication(duplication, facts, evidence.subject, { runId: evidence.analysis.runId, auditId: evidence.analysis.auditId });
216
+ validateCoverage(coverage, facts, evidence.subject, { runId: evidence.analysis.runId, auditId: evidence.analysis.auditId });
217
+ if (profile.decomposition)
218
+ validateDecomposition(decomposition, facts, evidence.subject, { runId: evidence.analysis.runId, auditId: evidence.analysis.auditId });
219
+ if (profile.errorHandling)
220
+ validateErrorHandling(errorHandling, facts, evidence.subject, { runId: evidence.analysis.runId, auditId: evidence.analysis.auditId });
221
+ if (profile.context)
222
+ validateErrorContext(context, errorHandling);
223
+ if (profile.transfers)
224
+ validateTransfers(transfers, context);
225
+ if (profile.operations)
226
+ validateOperations(operations, transfers);
227
+ if (profile.asyncUsage)
228
+ validateAsyncUsage(asyncReview, facts, evidence.subject, { runId: evidence.analysis.runId, auditId: evidence.analysis.auditId }, decomposition);
229
+ if (profile.asyncContext)
230
+ validateAsyncContext(asyncContext, asyncReview);
231
+ if (profile.asyncAdapters)
232
+ validateAsyncAdapters(asyncAdapters, asyncReview, asyncContext);
233
+ if (profile.asyncScoring) {
234
+ validateAsyncHelpers(asyncHelpers, asyncReview, asyncContext, asyncAdapters);
235
+ validateAsyncScoring(asyncScoring, asyncHelpers, asyncReview);
236
+ }
237
+ }
238
+ if (evidence.analysis.status === "complete" && (facts?.status !== "complete" || duplication?.status !== "complete" || coverage?.status !== "complete" || (profile.decomposition && decomposition?.status !== "complete") || (profile.errorHandling && errorHandling?.status !== "complete") || (profile.context && context?.status !== "complete") || (profile.transfers && transfers?.status !== "complete") || (profile.operations && operations?.status !== "complete") || (profile.asyncUsage && asyncReview?.status !== "complete") || (profile.asyncContext && asyncContext?.status !== "complete") || (profile.asyncAdapters && asyncAdapters?.status !== "complete") || (profile.asyncScoring && (asyncHelpers?.status !== "complete" || asyncScoring?.status !== "complete"))))
239
+ throw new Error("Complete Python evidence requires complete measurements.");
240
+ if (evidence.analysis.status === "incomplete" && facts?.status === "complete" && duplication?.status === "complete" && coverage?.status === "complete" && (!profile.decomposition || decomposition?.status === "complete") && (!profile.errorHandling || errorHandling?.status === "complete") && (!profile.context || context?.status === "complete") && (!profile.transfers || transfers?.status === "complete") && (!profile.operations || operations?.status === "complete") && (!profile.asyncUsage || asyncReview?.status === "complete") && (!profile.asyncContext || asyncContext?.status === "complete") && (!profile.asyncAdapters || asyncAdapters?.status === "complete") && (!profile.asyncScoring || asyncHelpers?.status === "complete" && asyncScoring?.status === "complete"))
241
+ throw new Error("Incomplete Python evidence must identify an incomplete measurement.");
242
+ if (evidence.analysis.status === "incomplete" && !evidence.analysis.diagnostics.length)
243
+ throw new Error("Incomplete Python evidence requires diagnostics.");
244
+ const expected = buildEvidence(profile, facts, evidence.subject, { runId: evidence.analysis.runId, auditId: evidence.analysis.auditId }, evidence.analysis.status === "incomplete" ? "Validation placeholder" : undefined, duplication, coverage, decomposition, errorHandling, context, transfers, operations, asyncReview, asyncContext, asyncAdapters, asyncHelpers, asyncScoring);
245
+ for (const key of ["filters", "population", "dimensions"])
246
+ if (!isDeepStrictEqual(evidence[key], expected[key]))
247
+ throw new Error(`Python ${key} do not match measured facts and experimental policy.`);
248
+ if (evidence.analysis.configurationFingerprint !== expected.analysis.configurationFingerprint)
249
+ throw new Error("Python configuration fingerprint does not match source facts.");
250
+ }
251
+ /** Historical previews and unavailable populations cannot participate in score comparisons. */
252
+ export function supportsPythonComparison(evidence) {
253
+ return [currentProfile, adapterProfile, asyncContextProfile, asyncProfile, operationProfile, transferProfile, contextProfile, handlerProfile, complexityProfile].some(profile => evidence.tool.version === profile.version && evidence.analysis.ruleset === profile.ruleset) &&
254
+ evidence.analysis.calibration === pythonCalibration && evidence.dimensions.codeQuality.status === "scored";
255
+ }
@@ -1,12 +1 @@
1
- export interface Fraction {
2
- numerator: bigint;
3
- denominator: bigint;
4
- }
5
- export declare function fraction(numerator: bigint, denominator?: bigint): Fraction;
6
- export declare function decimal(value: number): Fraction;
7
- export declare const add: (a: Fraction, b: Fraction) => Fraction;
8
- export declare const multiply: (a: Fraction, b: Fraction) => Fraction;
9
- export declare const subtract: (a: Fraction, b: Fraction) => Fraction;
10
- export declare const numeric: (a: Fraction) => number;
11
- export declare function roundScore(a: Fraction): number;
12
- export declare function interpolate(value: number, anchors: readonly (readonly [number, number])[]): Fraction;
1
+ export * from "#source-facts/arithmetic.mjs";
@@ -1,40 +1 @@
1
- export function fraction(numerator, denominator = 1n) {
2
- if (denominator <= 0n)
3
- throw new Error("Positive denominator required.");
4
- let a = numerator < 0n ? -numerator : numerator, b = denominator;
5
- while (b) {
6
- const remainder = a % b;
7
- a = b;
8
- b = remainder;
9
- }
10
- return { numerator: numerator / a, denominator: denominator / a };
11
- }
12
- export function decimal(value) {
13
- if (!Number.isFinite(value))
14
- throw new Error("Finite score measurement required.");
15
- const [mantissa, exponent = "0"] = String(value).toLowerCase().split("e");
16
- const places = (mantissa.split(".")[1] ?? "").length - Number(exponent);
17
- const numerator = BigInt(mantissa.replace(".", ""));
18
- return places >= 0 ? fraction(numerator, 10n ** BigInt(places)) : fraction(numerator * 10n ** BigInt(-places));
19
- }
20
- export const add = (a, b) => fraction(a.numerator * b.denominator + b.numerator * a.denominator, a.denominator * b.denominator);
21
- export const multiply = (a, b) => fraction(a.numerator * b.numerator, a.denominator * b.denominator);
22
- export const subtract = (a, b) => add(a, { ...b, numerator: -b.numerator });
23
- export const numeric = (a) => Number(a.numerator) / Number(a.denominator);
24
- export function roundScore(a) {
25
- if (a.numerator < 0n)
26
- throw new Error("Score must be nonnegative.");
27
- return Number((a.numerator * 20n + a.denominator) / (2n * a.denominator)) / 10;
28
- }
29
- export function interpolate(value, anchors) {
30
- if (!Number.isFinite(value))
31
- throw new Error("Finite score measurement required.");
32
- if (value <= anchors[0][0])
33
- return decimal(anchors[0][1]);
34
- for (let index = 1; index < anchors.length; index++) {
35
- const [x, y] = anchors[index], [previousX, previousY] = anchors[index - 1];
36
- if (value <= x)
37
- return add(decimal(previousY), multiply(subtract(decimal(value), decimal(previousX)), fraction(BigInt(y - previousY), BigInt(x - previousX))));
38
- }
39
- return decimal(anchors[anchors.length - 1][1]);
40
- }
1
+ export * from "#source-facts/arithmetic.mjs";
@@ -0,0 +1,9 @@
1
+ export interface Fraction { numerator: bigint; denominator: bigint; }
2
+ export function fraction(numerator: bigint, denominator?: bigint): Fraction;
3
+ export function decimal(value: number): Fraction;
4
+ export function add(a: Fraction, b: Fraction): Fraction;
5
+ export function multiply(a: Fraction, b: Fraction): Fraction;
6
+ export function subtract(a: Fraction, b: Fraction): Fraction;
7
+ export function numeric(a: Fraction): number;
8
+ export function roundScore(a: Fraction): number;
9
+ export function interpolate(value: number, anchors: readonly (readonly [number, number])[]): Fraction;
@@ -0,0 +1,32 @@
1
+ // Decimal inputs retain their measurement precision; subsequent arithmetic is exact.
2
+ export function fraction(numerator, denominator = 1n) {
3
+ if (denominator <= 0n) throw new Error('Positive denominator required.');
4
+ let a = numerator < 0n ? -numerator : numerator, b = denominator;
5
+ while (b) { const remainder = a % b; a = b; b = remainder; }
6
+ return { numerator: numerator / a, denominator: denominator / a };
7
+ }
8
+ export function decimal(value) {
9
+ if (!Number.isFinite(value)) throw new Error('Finite score measurement required.');
10
+ const [mantissa, exponent = '0'] = String(value).toLowerCase().split('e');
11
+ const places = (mantissa.split('.')[1] ?? '').length - Number(exponent);
12
+ const numerator = BigInt(mantissa.replace('.', ''));
13
+ return places >= 0 ? fraction(numerator, 10n ** BigInt(places)) : fraction(numerator * 10n ** BigInt(-places));
14
+ }
15
+ export const add = (a, b) => fraction(a.numerator * b.denominator + b.numerator * a.denominator, a.denominator * b.denominator);
16
+ export const multiply = (a, b) => fraction(a.numerator * b.numerator, a.denominator * b.denominator);
17
+ export const subtract = (a, b) => add(a, { ...b, numerator: -b.numerator });
18
+ export const numeric = a => Number(a.numerator) / Number(a.denominator);
19
+ export function roundScore(a) {
20
+ if (a.numerator < 0n) throw new Error('Score must be nonnegative.');
21
+ return Number((a.numerator * 20n + a.denominator) / (2n * a.denominator)) / 10;
22
+ }
23
+ export function interpolate(value, anchors) {
24
+ if (!Number.isFinite(value)) throw new Error('Finite score measurement required.');
25
+ if (value <= anchors[0][0]) return decimal(anchors[0][1]);
26
+ for (let index = 1; index < anchors.length; index++) {
27
+ const [x, y] = anchors[index], [previousX, previousY] = anchors[index - 1];
28
+ if (value <= x) return add(decimal(previousY), multiply(subtract(decimal(value), decimal(previousX)),
29
+ fraction(BigInt(y - previousY), BigInt(x - previousX))));
30
+ }
31
+ return decimal(anchors[anchors.length - 1][1]);
32
+ }
@@ -0,0 +1,35 @@
1
+ /** Internal adapter contract, deliberately separate from scorecard evidence v3. */
2
+ export interface SourceFacts {
3
+ schemaVersion: 1 | 2;
4
+ language: string;
5
+ producer: { name: string; version: string; parser: string; parserVersion: string };
6
+ measurementPolicy: string;
7
+ status: 'complete' | 'incomplete';
8
+ capabilities: { functionComplexity: 'available' | 'unsupported'; coverage: 'unsupported'; maintainability: 'unsupported' };
9
+ scope: { description: string; excludes: string[] };
10
+ diagnostics: { code: string; message: string; file?: string }[];
11
+ functions: FunctionFact[];
12
+ /** Required for v2; forbidden for v1. */
13
+ selection?: SelectionInventory;
14
+ }
15
+ export interface FunctionFact {
16
+ id: string;
17
+ file: string;
18
+ member: string;
19
+ line: number;
20
+ /** Required for v2 facts. V1 facts retain the producing analyzer's selection. */
21
+ population?: 'production' | 'test' | 'support' | 'generated';
22
+ /** Each body owns its decisions; nested function/class bodies are separate. */
23
+ ownership: 'exclusive-function-body';
24
+ complexity: { value: number; baseline: 1; decisions: Record<string, number> | null } | null;
25
+ }
26
+ export interface SelectionInventory {
27
+ policy: string; configurationFingerprint: string;
28
+ options: { include: string[]; exclude: string[]; testPaths: string[]; productionPaths: string[];
29
+ includeTests: boolean; includeGenerated: boolean; includeSupport: boolean };
30
+ files: { file: string; category: 'production' | 'test' | 'support' | 'generated' | 'declaration';
31
+ disposition: 'analyzed' | 'excluded' | 'failed'; reason: string; contentSha256: string | null }[];
32
+ prunedDirectories: { path: string; reason: string }[];
33
+ excludedFunctions: { id: string; file: string; member: string; line: number; reason: string }[];
34
+ }
35
+ export function validateSourceFacts(value: unknown): asserts value is SourceFacts;
@@ -0,0 +1,95 @@
1
+ // Validate at the producer/consumer boundary. Unknown data must never imply a pass.
2
+ function requireValue(ok, message) {
3
+ if (!ok) throw new Error(`Invalid source facts: ${message}`);
4
+ }
5
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
6
+ const text = value => typeof value === 'string' && value.trim().length > 0;
7
+ const count = value => Number.isSafeInteger(value) && value >= 0;
8
+ function keys(value, allowed, label) {
9
+ requireValue(object(value), `${label} must be an object`);
10
+ requireValue(Object.keys(value).every(key => allowed.includes(key)), `${label} has unknown fields`);
11
+ }
12
+ export function validateSourceFacts(value) {
13
+ const v2 = value?.schemaVersion === 2;
14
+ keys(value, ['schemaVersion', 'language', 'producer', 'measurementPolicy', 'status', 'capabilities', 'scope', 'diagnostics', 'functions', ...(v2 ? ['selection'] : [])], 'document');
15
+ requireValue(value.schemaVersion === 1 || v2, 'unsupported schemaVersion');
16
+ requireValue(text(value.language) && text(value.measurementPolicy), 'language and measurementPolicy required');
17
+ keys(value.producer, ['name', 'version', 'parser', 'parserVersion'], 'producer');
18
+ requireValue(['name', 'version', 'parser', 'parserVersion'].every(key => text(value.producer[key])), 'producer provenance required');
19
+ requireValue(['complete', 'incomplete'].includes(value.status), 'invalid status');
20
+ keys(value.capabilities, ['functionComplexity', 'coverage', 'maintainability'], 'capabilities');
21
+ requireValue(['available', 'unsupported'].includes(value.capabilities.functionComplexity), 'invalid complexity capability');
22
+ requireValue(value.capabilities.coverage === 'unsupported' && value.capabilities.maintainability === 'unsupported', 'this contract only exchanges complexity');
23
+ keys(value.scope, ['description', 'excludes'], 'scope');
24
+ requireValue(text(value.scope.description) && Array.isArray(value.scope.excludes) && value.scope.excludes.every(text), 'invalid scope');
25
+ requireValue(Array.isArray(value.diagnostics), 'diagnostics must be an array');
26
+ for (const diagnostic of value.diagnostics) {
27
+ keys(diagnostic, ['code', 'message', 'file'], 'diagnostic');
28
+ requireValue(text(diagnostic.code) && text(diagnostic.message) && (diagnostic.file === undefined || text(diagnostic.file)), 'invalid diagnostic');
29
+ }
30
+ requireValue(value.status !== 'incomplete' || value.diagnostics.length > 0, 'incomplete analysis needs a diagnostic');
31
+ requireValue(value.status !== 'complete' || value.diagnostics.length === 0, 'complete analysis cannot contain failure diagnostics');
32
+ requireValue(Array.isArray(value.functions), 'functions must be an array');
33
+ const ids = new Set();
34
+ for (const item of value.functions) {
35
+ keys(item, ['id', 'file', 'member', 'line', 'ownership', 'complexity', ...(v2 ? ['population'] : [])], 'function');
36
+ requireValue(text(item.id) && !ids.has(item.id), 'function IDs must be nonempty and unique');
37
+ ids.add(item.id);
38
+ requireValue(text(item.file) && text(item.member) && count(item.line) && item.line > 0, 'function source location required');
39
+ requireValue(item.ownership === 'exclusive-function-body', 'unsupported ownership policy');
40
+ if (v2) requireValue(['production', 'test', 'support', 'generated'].includes(item.population), 'invalid function population');
41
+ if (value.capabilities.functionComplexity === 'unsupported') {
42
+ requireValue(item.complexity === null, 'unsupported complexity must be null');
43
+ continue;
44
+ }
45
+ keys(item.complexity, ['value', 'baseline', 'decisions'], 'complexity');
46
+ requireValue(count(item.complexity.value) && item.complexity.value >= 1 && item.complexity.baseline === 1, 'invalid complexity value/baseline');
47
+ const decisions = item.complexity.decisions;
48
+ requireValue(decisions === null || object(decisions), 'decisions must be counts or null');
49
+ if (decisions !== null) {
50
+ requireValue(Object.entries(decisions).every(([key, n]) => text(key) && count(n)), 'invalid decision counts');
51
+ requireValue(1 + Object.values(decisions).reduce((a, b) => a + b, 0) === item.complexity.value, 'decision counts disagree with complexity');
52
+ }
53
+ }
54
+ if (v2) validateSelection(value, ids);
55
+ }
56
+
57
+ function validateSelection(document, ids) {
58
+ const value = document.selection;
59
+ const digest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
60
+ keys(value, ['policy', 'configurationFingerprint', 'options', 'files', 'prunedDirectories', 'excludedFunctions'], 'selection');
61
+ requireValue(text(value.policy) && digest(value.configurationFingerprint), 'selection provenance required');
62
+ const lists = ['include', 'exclude', 'testPaths', 'productionPaths'];
63
+ const flags = ['includeTests', 'includeGenerated', 'includeSupport'];
64
+ keys(value.options, [...lists, ...flags], 'selection options');
65
+ requireValue(lists.every(key => Array.isArray(value.options[key]) && value.options[key].every(text)) &&
66
+ flags.every(key => typeof value.options[key] === 'boolean'), 'invalid selection options');
67
+ requireValue(Array.isArray(value.files) && Array.isArray(value.prunedDirectories) && Array.isArray(value.excludedFunctions), 'selection inventories required');
68
+ const files = new Map();
69
+ for (const item of value.files) {
70
+ keys(item, ['file', 'category', 'disposition', 'reason', 'contentSha256'], 'selected file');
71
+ requireValue(text(item.file) && !files.has(item.file) && text(item.reason), 'unique files and reasons required');
72
+ requireValue(['production', 'test', 'support', 'generated', 'declaration'].includes(item.category) &&
73
+ ['analyzed', 'excluded', 'failed'].includes(item.disposition), 'invalid file classification');
74
+ requireValue(item.contentSha256 === null || digest(item.contentSha256), 'invalid source digest');
75
+ requireValue(item.disposition !== 'analyzed' || (digest(item.contentSha256) && item.category !== 'declaration'), 'analyzed implementation source required');
76
+ const flag = { test: 'includeTests', support: 'includeSupport', generated: 'includeGenerated' }[item.category];
77
+ requireValue(item.disposition !== 'analyzed' || !flag || value.options[flag], 'population was not enabled by selection options');
78
+ requireValue(item.disposition !== 'failed' || document.status === 'incomplete', 'failed files require incomplete analysis');
79
+ files.set(item.file, item);
80
+ }
81
+ for (const item of value.prunedDirectories) {
82
+ keys(item, ['path', 'reason'], 'pruned directory');
83
+ requireValue(text(item.path) && text(item.reason), 'pruned directory provenance required');
84
+ }
85
+ for (const item of document.functions) {
86
+ const file = files.get(item.file);
87
+ requireValue(file?.disposition === 'analyzed' && file.category === item.population, 'function does not belong to its analyzed population');
88
+ }
89
+ for (const item of value.excludedFunctions) {
90
+ keys(item, ['id', 'file', 'member', 'line', 'reason'], 'excluded function');
91
+ requireValue(text(item.id) && !ids.has(item.id) && text(item.member) && count(item.line) && item.line > 0 && text(item.reason), 'invalid or duplicate excluded function');
92
+ requireValue(files.get(item.file)?.disposition === 'analyzed', 'excluded declaration needs an analyzed file');
93
+ ids.add(item.id);
94
+ }
95
+ }
@@ -0,0 +1,12 @@
1
+ import type { SourceFacts } from './contract.mjs';
2
+ export const complexityAnchors: readonly (readonly [number, number])[];
3
+ export interface Contribution {
4
+ id: string; group: 'weakest' | 'remainder'; individualScore: number;
5
+ weightNumerator: number; weightDenominator: number; weightedScoreLoss: number;
6
+ }
7
+ export interface PopulationResult {
8
+ weakCount: number; restCount: number; contributions: Contribution[];
9
+ losses: Record<string, number>; unroundedScore: number; score: number;
10
+ }
11
+ export function aggregatePopulation(measurements: {id: string; value: number}[], anchors: readonly (readonly [number, number])[], higherIsWorse: boolean, weakCount: number): PopulationResult | null;
12
+ export function complexityPopulation(facts: SourceFacts): PopulationResult | null;
@@ -0,0 +1,41 @@
1
+ import { add, decimal, fraction, interpolate, multiply, numeric, roundScore, subtract } from './arithmetic.mjs';
2
+ import { validateSourceFacts } from './contract.mjs';
3
+
4
+ export const complexityAnchors = Object.freeze([[3, 10], [5, 8], [10, 6], [20, 4], [40, 0]].map(Object.freeze));
5
+
6
+ /** Shared policy mechanics; selecting/calibrating a language policy remains explicit. */
7
+ export function aggregatePopulation(measurements, anchors, higherIsWorse, weakCount) {
8
+ if (!measurements.length) return null;
9
+ if (!Number.isSafeInteger(weakCount) || weakCount < 1 || weakCount > measurements.length) throw new Error('Invalid weakest population size.');
10
+ if (!Array.isArray(anchors) || anchors.length < 2 || anchors.some(([x, y], i) => !Number.isSafeInteger(x) ||
11
+ !Number.isSafeInteger(y) || y < 0 || y > 10 || (i > 0 && x <= anchors[i - 1][0]))) throw new Error('Invalid score anchors.');
12
+ const ids = new Set();
13
+ for (const item of measurements) {
14
+ if (!item.id || ids.has(item.id) || !Number.isFinite(item.value)) throw new Error('Unique IDs and finite measurements required.');
15
+ ids.add(item.id);
16
+ }
17
+ const ordered = measurements.map(item => ({ item, score: interpolate(item.value, anchors) }))
18
+ .sort((a, b) => (higherIsWorse ? b.item.value - a.item.value : a.item.value - b.item.value) ||
19
+ (a.item.id < b.item.id ? -1 : a.item.id > b.item.id ? 1 : 0));
20
+ const restCount = ordered.length - weakCount;
21
+ const weakWeight = restCount ? fraction(2n, 5n * BigInt(weakCount)) : fraction(1n, BigInt(weakCount));
22
+ const restWeight = restCount ? fraction(3n, 5n * BigInt(restCount)) : decimal(0);
23
+ const losses = { weakest: decimal(0), remainder: decimal(0) };
24
+ const contributions = ordered.map(({ item, score }, index) => {
25
+ const group = index < weakCount ? 'weakest' : 'remainder';
26
+ const weight = index < weakCount ? weakWeight : restWeight;
27
+ const weightedLoss = multiply(subtract(decimal(10), score), weight);
28
+ losses[group] = add(losses[group], weightedLoss);
29
+ return { id: item.id, group, individualScore: numeric(score), weightNumerator: Number(weight.numerator),
30
+ weightDenominator: Number(weight.denominator), weightedScoreLoss: numeric(weightedLoss) };
31
+ });
32
+ const total = subtract(decimal(10), add(losses.weakest, losses.remainder));
33
+ return { weakCount, restCount, contributions, losses: { weakest: numeric(losses.weakest), remainder: numeric(losses.remainder) },
34
+ unroundedScore: numeric(total), score: roundScore(total) };
35
+ }
36
+
37
+ export function complexityPopulation(facts) {
38
+ validateSourceFacts(facts);
39
+ if (facts.status !== 'complete' || facts.capabilities.functionComplexity !== 'available') return null;
40
+ return aggregatePopulation(facts.functions.map(item => ({ id: item.id, value: item.complexity.value })), complexityAnchors, true, 1);
41
+ }
@@ -0,0 +1,4 @@
1
+ import type { FunctionMeasurement } from './function-measurements.js';
2
+ import type { SourceFacts } from '#source-facts/contract.mjs';
3
+ /** Adapt the existing selected population; this does not rediscover or remeasure files. */
4
+ export declare function functionSourceFacts(functions: FunctionMeasurement[]): SourceFacts;
@@ -0,0 +1,16 @@
1
+ import ts from 'typescript';
2
+ /** Adapt the existing selected population; this does not rediscover or remeasure files. */
3
+ export function functionSourceFacts(functions) {
4
+ return {
5
+ schemaVersion: 1, language: 'javascript-typescript',
6
+ producer: { name: 'codemetrics-ai/function-adapter', version: '1', parser: 'typescript', parserVersion: ts.version },
7
+ measurementPolicy: 'owned-function-body-v2-type-erasure', status: 'complete',
8
+ capabilities: { functionComplexity: 'available', coverage: 'unsupported', maintainability: 'unsupported' },
9
+ scope: { description: 'Existing selected implemented-function population supplied by the JS/TS analyzer.',
10
+ excludes: ['nested-function-and-class-bodies', 'type-only-syntax', 'parameter-initializers', 'module-and-field-initializers'] },
11
+ diagnostics: [],
12
+ functions: functions.map(item => ({ id: item.id, file: item.file, member: item.member, line: item.line,
13
+ ownership: 'exclusive-function-body', complexity: { value: item.ownComplexity, baseline: 1,
14
+ decisions: item.complexityBreakdown ? { ...item.complexityBreakdown.counts } : null } }))
15
+ };
16
+ }