codemetrics-ai 0.2.0

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.
@@ -0,0 +1,85 @@
1
+ import type { Evidence, Finding } from "./evidence.js";
2
+ export declare function validateEvidence(value: unknown): asserts value is Evidence;
3
+ export declare function readEvidence(file: string): Evidence;
4
+ export declare function findings(evidence: Evidence): Finding[];
5
+ export declare function compare(current: Evidence, baseline: Evidence, allowIncompatible?: boolean): {
6
+ compatible: boolean;
7
+ reasons: string[];
8
+ currentRun: {
9
+ runId: string | null;
10
+ auditId: string | null;
11
+ };
12
+ baselineRun: {
13
+ runId: string | null;
14
+ auditId: string | null;
15
+ };
16
+ new: Finding[];
17
+ resolved: Finding[];
18
+ changed: Finding[];
19
+ severityIncreases: Finding[];
20
+ unchanged: Finding[];
21
+ dimensions: {
22
+ [k: string]: {
23
+ before: number | null;
24
+ after: number | null;
25
+ delta: number | null;
26
+ };
27
+ };
28
+ };
29
+ export declare function gate(comparison: ReturnType<typeof compare>, severity?: string, maxScoreDrop?: number): boolean;
30
+ export declare function sarif(evidence: Evidence): {
31
+ $schema: string;
32
+ version: string;
33
+ runs: {
34
+ properties: {
35
+ runId: string | null;
36
+ auditId: string | null;
37
+ };
38
+ tool: {
39
+ driver: {
40
+ name: string;
41
+ version: string;
42
+ rules: {
43
+ id: string;
44
+ shortDescription: {
45
+ text: string;
46
+ };
47
+ help: {
48
+ text: string;
49
+ };
50
+ }[];
51
+ };
52
+ };
53
+ invocations: {
54
+ workingDirectory: {
55
+ uri: string;
56
+ };
57
+ executionSuccessful: boolean;
58
+ }[];
59
+ results: {
60
+ locations?: {
61
+ physicalLocation: {
62
+ region?: {
63
+ startLine: number;
64
+ } | undefined;
65
+ artifactLocation: {
66
+ uri: string;
67
+ };
68
+ };
69
+ }[] | undefined;
70
+ ruleId: string;
71
+ ruleIndex: number;
72
+ level: string;
73
+ message: {
74
+ text: string;
75
+ };
76
+ partialFingerprints: {
77
+ "codemetrics/v1": string;
78
+ };
79
+ properties: {
80
+ confidence: "high" | "medium" | "low";
81
+ observations: Record<string, unknown>;
82
+ };
83
+ }[];
84
+ }[];
85
+ };
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { Ajv2020 } from "ajv/dist/2020.js";
5
+ import addFormats from "ajv-formats";
6
+ import { dimensionKeys } from "./scorecard-contract.js";
7
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
8
+ addFormats(ajv);
9
+ let validator;
10
+ export function validateEvidence(value) {
11
+ if (value?.schemaVersion !== 3)
12
+ throw new Error("Schema-v3 evidence is required. Re-run the analyzer; v2 lacks comparison provenance.");
13
+ validator ??= ajv.compile(JSON.parse(fs.readFileSync(new URL("./evidence.schema.v3.json", import.meta.url), "utf8")));
14
+ if (!validator(value))
15
+ throw new Error("Invalid evidence: " + ajv.errorsText(validator.errors));
16
+ const evidence = value;
17
+ if (evidence.filters.analyzedUnits > evidence.filters.totalUnits)
18
+ throw new Error("Analyzed units exceed total units.");
19
+ const ids = findings(evidence).map(finding => finding.fingerprint);
20
+ if (new Set(ids).size !== ids.length)
21
+ throw new Error("Duplicate finding fingerprints.");
22
+ }
23
+ export function readEvidence(file) {
24
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
25
+ validateEvidence(value);
26
+ return value;
27
+ }
28
+ export function findings(evidence) { return dimensionKeys.flatMap(key => evidence.dimensions[key].findings); }
29
+ const ranks = { info: 0, warning: 1, error: 2 };
30
+ function entryPoint(evidence) {
31
+ const root = evidence.subject.root.replaceAll("\\", "/").replace(/\/$/, "") + "/";
32
+ const entry = evidence.subject.entryPoint.replaceAll("\\", "/");
33
+ return entry.startsWith(root) ? entry.slice(root.length) : entry;
34
+ }
35
+ export function compare(current, baseline, allowIncompatible = false) {
36
+ const reasons = [];
37
+ for (const key of ["ecosystem", "version", "name"])
38
+ if (current.tool[key] !== baseline.tool[key])
39
+ reasons.push("tool." + key + " differs");
40
+ for (const key of ["ruleset", "configurationFingerprint"])
41
+ if (current.analysis[key] !== baseline.analysis[key])
42
+ reasons.push("analysis." + key + " differs");
43
+ for (const key of ["name", "variant"])
44
+ if (current.subject[key] !== baseline.subject[key])
45
+ reasons.push("subject." + key + " differs");
46
+ if (entryPoint(current) !== entryPoint(baseline))
47
+ reasons.push("subject.entryPoint differs");
48
+ if (current.analysis.status !== "complete" || baseline.analysis.status !== "complete")
49
+ reasons.push("Analysis is incomplete");
50
+ if (dimensionKeys.some(key => current.dimensions[key].status === "failed" || baseline.dimensions[key].status === "failed"))
51
+ reasons.push("A dimension failed");
52
+ if (dimensionKeys.some(key => current.dimensions[key].status !== baseline.dimensions[key].status))
53
+ reasons.push("Dimension availability differs");
54
+ const scopeIdentity = (scope) => scope === undefined ? "unknown" :
55
+ JSON.stringify([scope.id, scope.coverage, [...scope.includes].sort(), [...scope.excludes].sort()]);
56
+ if (dimensionKeys.some(key => scopeIdentity(current.dimensions[key].scope) !== scopeIdentity(baseline.dimensions[key].scope)))
57
+ reasons.push("Dimension scope differs");
58
+ if (reasons.length && !allowIncompatible)
59
+ throw new Error("Incompatible baseline: " + reasons.join("; "));
60
+ const before = new Map(findings(baseline).map(finding => [finding.fingerprint, finding]));
61
+ const after = new Map(findings(current).map(finding => [finding.fingerprint, finding]));
62
+ const changed = findings(current).filter(finding => before.has(finding.fingerprint) &&
63
+ (before.get(finding.fingerprint).severity !== finding.severity || before.get(finding.fingerprint).confidence !== finding.confidence));
64
+ return { compatible: reasons.length === 0, reasons,
65
+ currentRun: { runId: current.analysis.runId ?? null, auditId: current.analysis.auditId ?? null },
66
+ baselineRun: { runId: baseline.analysis.runId ?? null, auditId: baseline.analysis.auditId ?? null },
67
+ new: findings(current).filter(finding => !before.has(finding.fingerprint)),
68
+ resolved: findings(baseline).filter(finding => !after.has(finding.fingerprint)), changed,
69
+ severityIncreases: changed.filter(finding => ranks[finding.severity] > ranks[before.get(finding.fingerprint).severity]),
70
+ unchanged: findings(current).filter(finding => before.has(finding.fingerprint) && !changed.includes(finding)),
71
+ dimensions: Object.fromEntries(dimensionKeys.map(key => [key, { before: baseline.dimensions[key].score ?? null,
72
+ after: current.dimensions[key].score ?? null,
73
+ delta: reasons.length || current.dimensions[key].score === undefined || baseline.dimensions[key].score === undefined ? null :
74
+ current.dimensions[key].score - baseline.dimensions[key].score }])) };
75
+ }
76
+ export function gate(comparison, severity, maxScoreDrop) {
77
+ if (!comparison.compatible)
78
+ throw new Error("Cannot apply quality gates to incompatible evidence.");
79
+ if (severity !== undefined && !(severity in ranks))
80
+ throw new Error("--fail-on-new must be info, warning, or error.");
81
+ if (maxScoreDrop !== undefined && (!Number.isFinite(maxScoreDrop) || maxScoreDrop < 0))
82
+ throw new Error("--max-score-drop must be a nonnegative number.");
83
+ return (severity !== undefined && [...comparison.new, ...comparison.severityIncreases].some(finding => ranks[finding.severity] >= ranks[severity])) ||
84
+ (maxScoreDrop !== undefined && Object.values(comparison.dimensions).some(dimension => dimension.delta !== null && dimension.delta < -maxScoreDrop));
85
+ }
86
+ export function sarif(evidence) {
87
+ const all = findings(evidence);
88
+ const ids = [...new Set(all.map(finding => finding.ruleId))].sort();
89
+ return { $schema: "https://json.schemastore.org/sarif-2.1.0.json", version: "2.1.0",
90
+ runs: [{ properties: { runId: evidence.analysis.runId ?? null, auditId: evidence.analysis.auditId ?? null },
91
+ tool: { driver: { name: evidence.tool.name, version: evidence.tool.version,
92
+ rules: ids.map(id => ({ id, shortDescription: { text: id.split("/").at(-1) },
93
+ help: { text: "Review observations and source context. Scores use aggregate dimension policies; findings are not independent deductions." } })) } },
94
+ invocations: [{ workingDirectory: { uri: pathToFileURL(evidence.subject.root + path.sep).href },
95
+ executionSuccessful: evidence.analysis.status === "complete" && !dimensionKeys.some(key => evidence.dimensions[key].status === "failed") }],
96
+ results: all.map(finding => ({ ruleId: finding.ruleId, ruleIndex: ids.indexOf(finding.ruleId),
97
+ level: finding.severity === "info" ? "note" : finding.severity, message: { text: finding.message },
98
+ partialFingerprints: { "codemetrics/v1": finding.fingerprint }, properties: { confidence: finding.confidence, observations: finding.observations },
99
+ ...(finding.file && !finding.file.startsWith("../") && !/^(?:[A-Za-z]:|\/)/.test(finding.file) ? {
100
+ locations: [{ physicalLocation: { artifactLocation: { uri: finding.file.split("/").map(encodeURIComponent).join("/") },
101
+ ...(finding.line && finding.line > 0 ? { region: { startLine: finding.line } } : {}) } }]
102
+ } : {}) })) }] };
103
+ }
@@ -0,0 +1,84 @@
1
+ import { type DimensionKey } from "./scorecard-contract.js";
2
+ export declare const hash: (text: string) => string;
3
+ export declare function invocationIds(runId?: string, auditId?: string): {
4
+ runId: string;
5
+ auditId: string;
6
+ };
7
+ export interface Finding {
8
+ category: string;
9
+ ruleId: string;
10
+ fingerprint: string;
11
+ severity: "info" | "warning" | "error";
12
+ confidence: "high" | "medium" | "low";
13
+ message: string;
14
+ file?: string;
15
+ line?: number;
16
+ project?: string;
17
+ type?: string;
18
+ member?: string;
19
+ package?: string;
20
+ observations: Record<string, unknown>;
21
+ }
22
+ export interface Dimension {
23
+ scope?: {
24
+ id: string;
25
+ coverage: "partial" | "unsupported";
26
+ includes: string[];
27
+ excludes: string[];
28
+ };
29
+ status: "scored" | "skipped" | "failed";
30
+ score?: number;
31
+ basis: string;
32
+ findings: Finding[];
33
+ [key: string]: unknown;
34
+ }
35
+ export interface Evidence {
36
+ schemaVersion: number;
37
+ generatedAtUtc: string;
38
+ tool: {
39
+ name: string;
40
+ version: string;
41
+ ecosystem: string;
42
+ };
43
+ subject: {
44
+ root: string;
45
+ entryPoint: string;
46
+ name?: string;
47
+ variant?: string;
48
+ };
49
+ filters: {
50
+ totalUnits: number;
51
+ analyzedUnits: number;
52
+ skipped: {
53
+ name: string;
54
+ reason: string;
55
+ }[];
56
+ };
57
+ population: {
58
+ types: number;
59
+ members: number;
60
+ };
61
+ dimensions: Record<DimensionKey, Dimension>;
62
+ analysis: {
63
+ runId?: string;
64
+ auditId?: string;
65
+ status: "complete" | "incomplete";
66
+ ruleset: string;
67
+ calibration: string;
68
+ configurationFingerprint: string;
69
+ diagnostics: {
70
+ kind: string;
71
+ message: string;
72
+ project?: string;
73
+ }[];
74
+ suppressions: {
75
+ file: string;
76
+ line: number;
77
+ categories: string[];
78
+ reason?: string;
79
+ status: string;
80
+ }[];
81
+ };
82
+ }
83
+ export declare function skippedDimensions(): Record<DimensionKey, Dimension>;
84
+ export declare function scored(score: number, basis: string, findings: Finding[], observations: Record<string, unknown>): Dimension;
@@ -0,0 +1,20 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { dimensionKeys } from "./scorecard-contract.js";
3
+ export const hash = (text) => createHash("sha256").update(text).digest("hex");
4
+ export function invocationIds(runId = randomUUID(), auditId = runId) {
5
+ for (const [name, value] of Object.entries({ runId, auditId }))
6
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value))
7
+ throw new Error(`${name} must be a UUID.`);
8
+ return { runId: runId.toLowerCase(), auditId: auditId.toLowerCase() };
9
+ }
10
+ export function skippedDimensions() {
11
+ return Object.fromEntries(dimensionKeys.map(key => [key, {
12
+ status: "skipped", basis: "Not implemented by this analyzer version.", findings: [],
13
+ scope: { id: `javascript-typescript/${key}/unsupported`, coverage: "unsupported", includes: [], excludes: [key] }
14
+ }]));
15
+ }
16
+ export function scored(score, basis, findings, observations) {
17
+ return { status: "scored", score, basis, findings: findings.sort((a, b) => a.fingerprint.localeCompare(b.fingerprint, "en")),
18
+ scoring: { algorithm: "dimension-policy", finalScore: score, aggregateScoreLoss: 10 - score,
19
+ contributionMode: "aggregate; findings are not independent deductions", observations } };
20
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/sean-m-cooper/CodeMetrics.AI/shared/scorecard-schema/evidence.schema.v2.json",
4
+ "title": "CodeMetrics.AI Evidence v2",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "generatedAtUtc", "tool", "subject", "filters", "population", "dimensions"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "schemaVersion": { "const": 2 },
10
+ "generatedAtUtc": { "type": "string", "format": "date-time" },
11
+ "tool": {
12
+ "type": "object",
13
+ "required": ["name", "version", "ecosystem"],
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "name": { "type": "string", "minLength": 1 },
17
+ "version": { "type": "string", "minLength": 1 },
18
+ "ecosystem": { "enum": ["dotnet", "javascript-typescript", "python", "rust"] }
19
+ }
20
+ },
21
+ "subject": {
22
+ "type": "object",
23
+ "required": ["root", "entryPoint"],
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "root": { "type": "string", "minLength": 1 },
27
+ "entryPoint": { "type": "string", "minLength": 1 },
28
+ "name": { "type": "string" },
29
+ "variant": { "type": "string" }
30
+ }
31
+ },
32
+ "filters": {
33
+ "type": "object",
34
+ "required": ["totalUnits", "analyzedUnits", "skipped"],
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "totalUnits": { "type": "integer", "minimum": 0 },
38
+ "analyzedUnits": { "type": "integer", "minimum": 0 },
39
+ "skipped": {
40
+ "type": "array",
41
+ "items": {
42
+ "type": "object",
43
+ "required": ["name", "reason"],
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "name": { "type": "string", "minLength": 1 },
47
+ "reason": { "type": "string", "minLength": 1 }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ },
53
+ "population": {
54
+ "type": "object",
55
+ "required": ["types", "members"],
56
+ "additionalProperties": false,
57
+ "properties": {
58
+ "types": { "type": "integer", "minimum": 0 },
59
+ "members": { "type": "integer", "minimum": 0 }
60
+ }
61
+ },
62
+ "dimensions": {
63
+ "type": "object",
64
+ "required": [
65
+ "codeQuality",
66
+ "maintainability",
67
+ "errorHandling",
68
+ "performanceAsync",
69
+ "security",
70
+ "testing",
71
+ "documentation",
72
+ "dependencyManagement",
73
+ "architecture"
74
+ ],
75
+ "additionalProperties": false,
76
+ "properties": {
77
+ "codeQuality": { "$ref": "#/$defs/dimensionResult" },
78
+ "maintainability": { "$ref": "#/$defs/dimensionResult" },
79
+ "errorHandling": { "$ref": "#/$defs/dimensionResult" },
80
+ "performanceAsync": { "$ref": "#/$defs/dimensionResult" },
81
+ "security": { "$ref": "#/$defs/dimensionResult" },
82
+ "testing": { "$ref": "#/$defs/dimensionResult" },
83
+ "documentation": { "$ref": "#/$defs/dimensionResult" },
84
+ "dependencyManagement": { "$ref": "#/$defs/dimensionResult" },
85
+ "architecture": { "$ref": "#/$defs/dimensionResult" }
86
+ }
87
+ }
88
+ },
89
+ "$defs": {
90
+ "dimensionResult": {
91
+ "type": "object",
92
+ "required": ["status", "basis", "findings"],
93
+ "properties": {
94
+ "status": { "enum": ["scored", "skipped", "failed"] },
95
+ "score": { "type": "number", "minimum": 0, "maximum": 10 },
96
+ "basis": { "type": "string", "minLength": 1 },
97
+ "findings": {
98
+ "type": "array",
99
+ "items": { "$ref": "#/$defs/finding" }
100
+ }
101
+ },
102
+ "additionalProperties": true,
103
+ "if": {
104
+ "properties": { "status": { "const": "scored" } },
105
+ "required": ["status"]
106
+ },
107
+ "then": { "required": ["score"] }
108
+ },
109
+ "finding": {
110
+ "type": "object",
111
+ "required": ["category", "severity", "message"],
112
+ "additionalProperties": false,
113
+ "properties": {
114
+ "category": { "type": "string", "minLength": 1 },
115
+ "severity": { "enum": ["info", "warning", "error"] },
116
+ "file": { "type": "string" },
117
+ "line": { "type": "integer", "minimum": 0 },
118
+ "project": { "type": "string" },
119
+ "type": { "type": "string" },
120
+ "member": { "type": "string" },
121
+ "package": { "type": "string" },
122
+ "confidence": { "enum": ["high", "medium", "low"] },
123
+ "message": { "type": "string", "minLength": 1 }
124
+ }
125
+ }
126
+ }
127
+ }