codecaliper 0.1.0__py3-none-any.whl
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.
- codecaliper/__init__.py +64 -0
- codecaliper/_version.py +1 -0
- codecaliper/api.py +446 -0
- codecaliper/canonical.py +130 -0
- codecaliper/cli.py +231 -0
- codecaliper/errors.py +27 -0
- codecaliper/languages/__init__.py +44 -0
- codecaliper/languages/base.py +162 -0
- codecaliper/languages/java.py +192 -0
- codecaliper/languages/python.py +179 -0
- codecaliper/metrics/__init__.py +0 -0
- codecaliper/metrics/base.py +55 -0
- codecaliper/metrics/cognitive.py +111 -0
- codecaliper/metrics/cyclomatic.py +62 -0
- codecaliper/metrics/halstead.py +55 -0
- codecaliper/metrics/loc.py +62 -0
- codecaliper/metrics/mi.py +31 -0
- codecaliper/model.py +132 -0
- codecaliper/py.typed +0 -0
- codecaliper/readability/__init__.py +13 -0
- codecaliper/readability/base.py +11 -0
- codecaliper/readability/bw2010.py +156 -0
- codecaliper/readability/granularity.py +210 -0
- codecaliper/readability/retrain.py +67 -0
- codecaliper/spec/__init__.py +3 -0
- codecaliper/spec/registry.py +112 -0
- codecaliper/spec/rulings/bw.toml +156 -0
- codecaliper/spec/rulings/cognitive.toml +124 -0
- codecaliper/spec/rulings/core.toml +87 -0
- codecaliper/spec/rulings/cyclomatic.toml +172 -0
- codecaliper/spec/rulings/halstead.toml +20 -0
- codecaliper/spec/rulings/index.toml +54 -0
- codecaliper/spec/rulings/loc.toml +50 -0
- codecaliper/spec/rulings/mi.toml +15 -0
- codecaliper/spec/rulings/tokenization.toml +191 -0
- codecaliper/spec/validated_grammars.toml +6 -0
- codecaliper/syntax/__init__.py +0 -0
- codecaliper/syntax/_treesitter.py +179 -0
- codecaliper/syntax/grammars.py +45 -0
- codecaliper/syntax/tokens.py +165 -0
- codecaliper-0.1.0.dist-info/METADATA +180 -0
- codecaliper-0.1.0.dist-info/RECORD +46 -0
- codecaliper-0.1.0.dist-info/WHEEL +4 -0
- codecaliper-0.1.0.dist-info/entry_points.txt +2 -0
- codecaliper-0.1.0.dist-info/licenses/LICENSE +21 -0
- codecaliper-0.1.0.dist-info/licenses/NOTICE +29 -0
codecaliper/model.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Result data model.
|
|
2
|
+
|
|
3
|
+
All result types are frozen, slotted dataclasses: immutable, clock-free (no
|
|
4
|
+
timestamps anywhere — outputs are byte-reproducible per platform, CORE-ALL-0004),
|
|
5
|
+
with a stable field order that canonical.py serializes deterministically.
|
|
6
|
+
|
|
7
|
+
Honesty is encoded in types, not docs: MI carries a typed ``derived_from`` and a
|
|
8
|
+
standing ``mi-contains-cc`` diagnostic; every Halstead value carries
|
|
9
|
+
``halstead-approximation``; readability results carry ``extrapolated`` as a field;
|
|
10
|
+
there is deliberately no score field anywhere (ARCHITECTURE.md §13).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
Granularity = Literal["snippet", "function", "file"]
|
|
19
|
+
Severity = Literal["info", "warning", "error"]
|
|
20
|
+
|
|
21
|
+
#: Closed set of diagnostic codes (ARCHITECTURE.md §2).
|
|
22
|
+
DIAGNOSTIC_CODES = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"parse-error-recovered",
|
|
25
|
+
"unvalidated-grammar",
|
|
26
|
+
"granularity-extrapolated",
|
|
27
|
+
"snippet-out-of-calibrated-range",
|
|
28
|
+
"snippet-scaffolded",
|
|
29
|
+
"bw-lexical-fallback",
|
|
30
|
+
"mi-contains-cc",
|
|
31
|
+
"halstead-approximation",
|
|
32
|
+
"encoding-replaced",
|
|
33
|
+
"bom-stripped",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class Span:
|
|
40
|
+
"""1-based line numbers, 0-based columns (editor convention)."""
|
|
41
|
+
|
|
42
|
+
start_line: int
|
|
43
|
+
start_col: int
|
|
44
|
+
end_line: int
|
|
45
|
+
end_col: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class GrammarInfo:
|
|
50
|
+
language: str # "python"
|
|
51
|
+
package: str # "tree-sitter-python"
|
|
52
|
+
version: str # actually-installed version (importlib.metadata)
|
|
53
|
+
abi_version: int # tree-sitter Language ABI
|
|
54
|
+
validated: bool # matches spec/validated_grammars.toml? Run and label, never refuse.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class Provenance:
|
|
59
|
+
tool_version: str
|
|
60
|
+
spec_version: str # on EVERY report — the instrument's calibration stamp
|
|
61
|
+
language: str
|
|
62
|
+
grammar: GrammarInfo
|
|
63
|
+
modes: tuple[tuple[str, str], ...] # (("cognitive", "whitepaper"),) — sorted, hashable
|
|
64
|
+
rulings_applied: tuple[str, ...] # sorted, deduped IDs governing the emitted values
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class Diagnostic:
|
|
69
|
+
severity: Severity
|
|
70
|
+
code: str # member of DIAGNOSTIC_CODES
|
|
71
|
+
message: str
|
|
72
|
+
span: Span | None = None
|
|
73
|
+
ruling: str | None = None # the ruling governing this behaviour, if any
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class RulingTrace:
|
|
78
|
+
"""Per-increment attribution, populated only with explain=True (zero default cost)."""
|
|
79
|
+
|
|
80
|
+
ruling_id: str
|
|
81
|
+
span: Span
|
|
82
|
+
delta: float
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class MetricValue:
|
|
87
|
+
metric: str # "cyclomatic", "halstead.volume", ...
|
|
88
|
+
value: int | float
|
|
89
|
+
rulings: tuple[str, ...] = () # ruling IDs governing this metric for this language
|
|
90
|
+
derived_from: tuple[str, ...] = () # MI: ("halstead.volume", "cyclomatic", "sloc")
|
|
91
|
+
trace: tuple[RulingTrace, ...] = ()
|
|
92
|
+
diagnostics: tuple[Diagnostic, ...] = ()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True, slots=True)
|
|
96
|
+
class FeatureVectorResult:
|
|
97
|
+
"""A raw feature vector — never a score (ARCHITECTURE.md §7/§13)."""
|
|
98
|
+
|
|
99
|
+
feature_set: str # "bw2010"
|
|
100
|
+
granularity: Granularity
|
|
101
|
+
native_granularity: Granularity # "snippet" for bw2010
|
|
102
|
+
extrapolated: bool # granularity != native_granularity
|
|
103
|
+
names: tuple[str, ...] # canonical order (bw2010: Fig. 6 order from the reference)
|
|
104
|
+
values: tuple[float, ...]
|
|
105
|
+
rulings: tuple[str, ...] = ()
|
|
106
|
+
diagnostics: tuple[Diagnostic, ...] = ()
|
|
107
|
+
unit_name: str | None = None # function name when granularity == "function"
|
|
108
|
+
span: Span | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class FunctionReport:
|
|
113
|
+
name: str
|
|
114
|
+
qualified_name: str
|
|
115
|
+
span: Span
|
|
116
|
+
metrics: tuple[MetricValue, ...] = ()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class FileReport:
|
|
121
|
+
path: str | None # None for in-memory measure()
|
|
122
|
+
parse_ok: bool # False if the tree contains ERROR/MISSING nodes (CORE-ALL-0002)
|
|
123
|
+
file_metrics: tuple[MetricValue, ...] = ()
|
|
124
|
+
functions: tuple[FunctionReport, ...] = ()
|
|
125
|
+
readability: tuple[FeatureVectorResult, ...] = () # plural: feature SETS, not scores
|
|
126
|
+
diagnostics: tuple[Diagnostic, ...] = ()
|
|
127
|
+
provenance: Provenance | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def metric_map(values: tuple[MetricValue, ...]) -> dict[str, MetricValue]:
|
|
131
|
+
"""Convenience view keyed by metric name (stable insertion order preserved)."""
|
|
132
|
+
return {v.metric: v for v in values}
|
codecaliper/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from codecaliper.readability.base import available_feature_sets
|
|
2
|
+
from codecaliper.readability.bw2010 import (
|
|
3
|
+
BW_FEATURE_NAMES,
|
|
4
|
+
BW_FEATURE_ORDER_SHA,
|
|
5
|
+
bw_features,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BW_FEATURE_NAMES",
|
|
10
|
+
"BW_FEATURE_ORDER_SHA",
|
|
11
|
+
"available_feature_sets",
|
|
12
|
+
"bw_features",
|
|
13
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""FeatureSet registry — bw2010 now; scalabrino2018/dorn2012 slot in at 1.x
|
|
2
|
+
without model changes (FileReport.readability is already plural)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
#: name -> native granularity; extraction dispatch lives in api.py for the MVP.
|
|
7
|
+
FEATURE_SETS: dict[str, str] = {"bw2010": "snippet"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def available_feature_sets() -> tuple[str, ...]:
|
|
11
|
+
return tuple(FEATURE_SETS)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""The 25 Buse-Weimer (2010, Fig. 6) readability features on tree-sitter.
|
|
2
|
+
|
|
3
|
+
Canonical feature names and order are inherited verbatim from the reference
|
|
4
|
+
implementation (Spaghetti Architect bench/anchor.py::_BW_FEATURE_NAMES,
|
|
5
|
+
NOTICE-credited); tests/test_bw_port_fidelity.py holds this port to the stdlib
|
|
6
|
+
reference per feature. Output is always the raw vector — there is no score
|
|
7
|
+
(BW-ALL-0001).
|
|
8
|
+
|
|
9
|
+
Two feature families (BW-ALL-0004):
|
|
10
|
+
- raw-line facts (length, indentation, spaces, blanks, char frequency) from the
|
|
11
|
+
normalized text of every line, strings included;
|
|
12
|
+
- token facts (identifiers, keywords, numbers, comments, operator classes,
|
|
13
|
+
punctuation) from the unified LexicalToken stream, so string/comment contents
|
|
14
|
+
are invisible to them.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
|
|
21
|
+
from codecaliper.languages.base import LanguageAdapter
|
|
22
|
+
from codecaliper.spec import require
|
|
23
|
+
from codecaliper.syntax.tokens import LexicalToken, TokenKind
|
|
24
|
+
|
|
25
|
+
R_FEATURES = require("BW-ALL-0001")
|
|
26
|
+
R_GRANULARITY = require("BW-ALL-0002")
|
|
27
|
+
R_COMMENT_ONCE = require("BW-ALL-0003")
|
|
28
|
+
R_TOKEN_LEVEL = require("BW-ALL-0004")
|
|
29
|
+
R_ID_LENGTH = require("BW-ALL-0005")
|
|
30
|
+
R_OP_CLASSES = require("BW-ALL-0006")
|
|
31
|
+
|
|
32
|
+
#: Fig. 6 order — verbatim from the reference implementation.
|
|
33
|
+
BW_FEATURE_NAMES: tuple[str, ...] = (
|
|
34
|
+
"avg_line_length", "max_line_length",
|
|
35
|
+
"avg_identifiers", "max_identifiers",
|
|
36
|
+
"avg_identifier_length", "max_identifier_length",
|
|
37
|
+
"avg_indentation", "max_indentation",
|
|
38
|
+
"avg_keywords", "max_keywords",
|
|
39
|
+
"avg_numbers", "max_numbers",
|
|
40
|
+
"avg_comments", "avg_periods", "avg_commas", "avg_spaces",
|
|
41
|
+
"avg_parentheses", "avg_arithmetic_ops", "avg_comparison_ops",
|
|
42
|
+
"avg_assignments", "avg_branches", "avg_loops", "avg_blank_lines",
|
|
43
|
+
"max_char_occurrences", "max_identifier_occurrences",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
#: Feature-order integrity stamp for trained-model artifacts (ARCHITECTURE.md §7.2).
|
|
47
|
+
BW_FEATURE_ORDER_SHA: str = hashlib.sha256(",".join(BW_FEATURE_NAMES).encode()).hexdigest()
|
|
48
|
+
|
|
49
|
+
#: The rulings in force for every bw2010 vector.
|
|
50
|
+
BW_RULINGS: tuple[str, ...] = (
|
|
51
|
+
R_FEATURES, R_GRANULARITY, R_COMMENT_ONCE, R_TOKEN_LEVEL, R_ID_LENGTH, R_OP_CLASSES,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
#: The paper's calibrated snippet regime (BW-ALL-0002).
|
|
55
|
+
CALIBRATED_LINE_RANGE = (4, 11)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def bw_features(
|
|
59
|
+
lines: list[str], tokens: list[LexicalToken], adapter: LanguageAdapter
|
|
60
|
+
) -> dict[str, float]:
|
|
61
|
+
"""Compute the 25 features. ``tokens`` line numbers must be 1-based relative
|
|
62
|
+
to ``lines`` (granularity slicing rebases them)."""
|
|
63
|
+
n_lines = max(1, len(lines))
|
|
64
|
+
|
|
65
|
+
line_len: list[int] = []
|
|
66
|
+
indent: list[int] = []
|
|
67
|
+
spaces = 0
|
|
68
|
+
blank = 0
|
|
69
|
+
char_freq: dict[str, int] = {}
|
|
70
|
+
for line in lines:
|
|
71
|
+
line_len.append(len(line))
|
|
72
|
+
stripped = line.lstrip()
|
|
73
|
+
# TOK-ALL-0006 (supersedes TOK-ALL-0004): a tab counts as 8 indentation
|
|
74
|
+
# characters, arbitrated by the BW faithfulness experiment. Line length
|
|
75
|
+
# above stays a raw character count.
|
|
76
|
+
leading = line[: len(line) - len(stripped)]
|
|
77
|
+
indent.append(sum(8 if ch == "\t" else 1 for ch in leading))
|
|
78
|
+
spaces += line.count(" ")
|
|
79
|
+
if stripped == "":
|
|
80
|
+
blank += 1
|
|
81
|
+
for ch in line:
|
|
82
|
+
char_freq[ch] = char_freq.get(ch, 0) + 1
|
|
83
|
+
|
|
84
|
+
ident_per_line = [0] * len(lines)
|
|
85
|
+
ident_len_per_line = [0] * len(lines) # per-line max identifier length (BW-ALL-0005)
|
|
86
|
+
kw_per_line = [0] * len(lines)
|
|
87
|
+
num_per_line = [0] * len(lines)
|
|
88
|
+
comments = periods = commas = parens = 0
|
|
89
|
+
arith = compare = assign = branch = loop = 0
|
|
90
|
+
ident_freq: dict[str, int] = {}
|
|
91
|
+
|
|
92
|
+
for tok in tokens:
|
|
93
|
+
i = tok.line - 1
|
|
94
|
+
if not (0 <= i < len(lines)):
|
|
95
|
+
continue
|
|
96
|
+
if tok.kind is TokenKind.IDENTIFIER:
|
|
97
|
+
ident_per_line[i] += 1
|
|
98
|
+
ident_len_per_line[i] = max(ident_len_per_line[i], len(tok.text))
|
|
99
|
+
ident_freq[tok.text] = ident_freq.get(tok.text, 0) + 1
|
|
100
|
+
elif tok.kind is TokenKind.KEYWORD:
|
|
101
|
+
kw_per_line[i] += 1
|
|
102
|
+
if tok.text in adapter.branch_keywords:
|
|
103
|
+
branch += 1
|
|
104
|
+
elif tok.text in adapter.loop_keywords:
|
|
105
|
+
loop += 1
|
|
106
|
+
elif tok.kind is TokenKind.NUMBER:
|
|
107
|
+
num_per_line[i] += 1
|
|
108
|
+
elif tok.kind is TokenKind.COMMENT:
|
|
109
|
+
comments += 1 # once per token (BW-ALL-0003, provisional)
|
|
110
|
+
elif tok.kind in (TokenKind.OPERATOR, TokenKind.PUNCT):
|
|
111
|
+
if tok.text == ",":
|
|
112
|
+
commas += 1
|
|
113
|
+
elif tok.text == ".":
|
|
114
|
+
periods += 1
|
|
115
|
+
elif tok.text in ("(", ")"):
|
|
116
|
+
parens += 1
|
|
117
|
+
if tok.text in adapter.assignment_ops:
|
|
118
|
+
assign += 1
|
|
119
|
+
elif tok.text in adapter.comparison_ops:
|
|
120
|
+
compare += 1
|
|
121
|
+
elif tok.text in adapter.arithmetic_ops:
|
|
122
|
+
arith += 1
|
|
123
|
+
|
|
124
|
+
def _avg(xs: list[int]) -> float:
|
|
125
|
+
return sum(xs) / len(xs) if xs else 0.0
|
|
126
|
+
|
|
127
|
+
def _max(xs: list[int]) -> float:
|
|
128
|
+
return float(max(xs)) if xs else 0.0
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"avg_line_length": _avg(line_len),
|
|
132
|
+
"max_line_length": _max(line_len),
|
|
133
|
+
"avg_identifiers": _avg(ident_per_line),
|
|
134
|
+
"max_identifiers": _max(ident_per_line),
|
|
135
|
+
"avg_identifier_length": _avg(ident_len_per_line),
|
|
136
|
+
"max_identifier_length": _max(ident_len_per_line),
|
|
137
|
+
"avg_indentation": _avg(indent),
|
|
138
|
+
"max_indentation": _max(indent),
|
|
139
|
+
"avg_keywords": _avg(kw_per_line),
|
|
140
|
+
"max_keywords": _max(kw_per_line),
|
|
141
|
+
"avg_numbers": _avg(num_per_line),
|
|
142
|
+
"max_numbers": _max(num_per_line),
|
|
143
|
+
"avg_comments": comments / n_lines,
|
|
144
|
+
"avg_periods": periods / n_lines,
|
|
145
|
+
"avg_commas": commas / n_lines,
|
|
146
|
+
"avg_spaces": spaces / n_lines,
|
|
147
|
+
"avg_parentheses": parens / n_lines,
|
|
148
|
+
"avg_arithmetic_ops": arith / n_lines,
|
|
149
|
+
"avg_comparison_ops": compare / n_lines,
|
|
150
|
+
"avg_assignments": assign / n_lines,
|
|
151
|
+
"avg_branches": branch / n_lines,
|
|
152
|
+
"avg_loops": loop / n_lines,
|
|
153
|
+
"avg_blank_lines": blank / n_lines,
|
|
154
|
+
"max_char_occurrences": float(max(char_freq.values(), default=0)),
|
|
155
|
+
"max_identifier_occurrences": float(max(ident_freq.values(), default=0)),
|
|
156
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Granularity segmentation and extrapolation labelling (BW-ALL-0002), plus the
|
|
2
|
+
Java bare-snippet scaffold (CORE-JAVA-0001)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from codecaliper.languages.base import FunctionUnit, LanguageAdapter
|
|
7
|
+
from codecaliper.model import Diagnostic, FeatureVectorResult, Granularity, Span
|
|
8
|
+
from codecaliper.readability.bw2010 import (
|
|
9
|
+
BW_FEATURE_NAMES,
|
|
10
|
+
BW_RULINGS,
|
|
11
|
+
CALIBRATED_LINE_RANGE,
|
|
12
|
+
bw_features,
|
|
13
|
+
)
|
|
14
|
+
from codecaliper.spec import require
|
|
15
|
+
from codecaliper.syntax.tokens import LexicalToken, lang_tokenization_rulings
|
|
16
|
+
|
|
17
|
+
R_TOK_LINE = require("TOK-ALL-0005") # token line attribution
|
|
18
|
+
R_TOK_TAB = require("TOK-ALL-0006") # tab = 8 indentation characters
|
|
19
|
+
|
|
20
|
+
JAVA_SNIPPET_PREFIX = "class __CC__ {\nvoid __cc__() {\n"
|
|
21
|
+
JAVA_SNIPPET_SUFFIX = "\n}\n}\n"
|
|
22
|
+
JAVA_SNIPPET_LINE_OFFSET = 2 # lines prepended by the class+method scaffold
|
|
23
|
+
|
|
24
|
+
# Class-body-only scaffold: rescues member-level snippets (constructors,
|
|
25
|
+
# modifier-bearing methods) that a method body cannot contain (CORE-JAVA-0001).
|
|
26
|
+
JAVA_CLASS_PREFIX = "class __CC__ {\n"
|
|
27
|
+
JAVA_CLASS_SUFFIX = "\n}\n"
|
|
28
|
+
JAVA_CLASS_LINE_OFFSET = 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rebase_unit(unit: FunctionUnit, line_offset: int, n_lines: int) -> FunctionUnit:
|
|
32
|
+
"""Shift a FunctionUnit's span from scaffold to original snippet coordinates,
|
|
33
|
+
and strip the synthetic scaffold qualifiers from qualified_name — scaffold
|
|
34
|
+
artifacts must never leak into emitted output (CORE-JAVA-0001). Longest
|
|
35
|
+
prefix first: the class+method scaffold nests units under __CC__.__cc__."""
|
|
36
|
+
qualified = unit.qualified_name
|
|
37
|
+
for scaffold_prefix in ("__CC__.__cc__.", "__CC__."):
|
|
38
|
+
if qualified.startswith(scaffold_prefix):
|
|
39
|
+
qualified = qualified[len(scaffold_prefix):]
|
|
40
|
+
break
|
|
41
|
+
s = unit.span
|
|
42
|
+
return FunctionUnit(
|
|
43
|
+
unit.name,
|
|
44
|
+
qualified,
|
|
45
|
+
unit.node,
|
|
46
|
+
Span(
|
|
47
|
+
start_line=s.start_line - line_offset,
|
|
48
|
+
start_col=s.start_col,
|
|
49
|
+
end_line=min(s.end_line - line_offset, n_lines),
|
|
50
|
+
end_col=s.end_col,
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rebase_tokens(
|
|
56
|
+
tokens: list[LexicalToken], start_line: int, end_line: int
|
|
57
|
+
) -> list[LexicalToken]:
|
|
58
|
+
"""Tokens whose start lies in [start_line, end_line], rebased to 1-based."""
|
|
59
|
+
offset = start_line - 1
|
|
60
|
+
return [
|
|
61
|
+
LexicalToken(t.kind, t.text, t.line - offset, min(t.end_line, end_line) - offset)
|
|
62
|
+
for t in tokens
|
|
63
|
+
if start_line <= t.line <= end_line
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _bw_vector_rulings(
|
|
68
|
+
adapter: LanguageAdapter, lexical_fallback: bool, cond_rulings: tuple[str, ...]
|
|
69
|
+
) -> tuple[str, ...]:
|
|
70
|
+
"""The governing set for an emitted BW vector: the six BW-ALL requires,
|
|
71
|
+
the language-specific bw2010 rulings (keyword/branch-keyword delimitation),
|
|
72
|
+
and the tokenization rulings that shape the features (TOK-ALL-0005 line
|
|
73
|
+
attribution, TOK-ALL-0006 tab=8 indentation, the language's static
|
|
74
|
+
atomic-string rulings). ``cond_rulings`` are the construct-specific
|
|
75
|
+
tokenization rulings (TOK-ALL-0007 anonymous words, TOK-JAVA-0002 `_`) that
|
|
76
|
+
actually engaged inside THIS vector's line span; BW-ALL-0007 stays strictly
|
|
77
|
+
conditional on the lexical fallback actually engaging."""
|
|
78
|
+
from codecaliper.spec import iter_rulings
|
|
79
|
+
|
|
80
|
+
lang_specific = tuple(sorted(
|
|
81
|
+
r.id
|
|
82
|
+
for r in iter_rulings(metric="bw2010", language=adapter.name)
|
|
83
|
+
if r.id not in BW_RULINGS and r.id != "BW-ALL-0007"
|
|
84
|
+
))
|
|
85
|
+
return (
|
|
86
|
+
BW_RULINGS
|
|
87
|
+
+ lang_specific
|
|
88
|
+
+ (R_TOK_LINE, R_TOK_TAB)
|
|
89
|
+
+ lang_tokenization_rulings(adapter)
|
|
90
|
+
+ tuple(sorted(cond_rulings))
|
|
91
|
+
+ (("BW-ALL-0007",) if lexical_fallback else ())
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def vector(
|
|
96
|
+
lines: list[str],
|
|
97
|
+
tokens: list[LexicalToken],
|
|
98
|
+
adapter: LanguageAdapter,
|
|
99
|
+
granularity: Granularity,
|
|
100
|
+
*,
|
|
101
|
+
unit_name: str | None = None,
|
|
102
|
+
span: Span | None = None,
|
|
103
|
+
scaffolded: bool = False,
|
|
104
|
+
lexical_fallback: bool = False,
|
|
105
|
+
cond_rulings: tuple[str, ...] = (),
|
|
106
|
+
) -> FeatureVectorResult:
|
|
107
|
+
features = bw_features(lines, tokens, adapter)
|
|
108
|
+
diags: list[Diagnostic] = []
|
|
109
|
+
if lexical_fallback:
|
|
110
|
+
diags.append(
|
|
111
|
+
Diagnostic(
|
|
112
|
+
"info", "bw-lexical-fallback",
|
|
113
|
+
"parse errors present; token-family features computed over the "
|
|
114
|
+
"full lexical stream, ERROR regions included — the BW construct "
|
|
115
|
+
"is lexical (BW-ALL-0007). CORE-ALL-0002 still governs every "
|
|
116
|
+
"metric.",
|
|
117
|
+
ruling="BW-ALL-0007",
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
extrapolated = granularity != "snippet"
|
|
121
|
+
if extrapolated:
|
|
122
|
+
diags.append(
|
|
123
|
+
Diagnostic(
|
|
124
|
+
"warning", "granularity-extrapolated",
|
|
125
|
+
f"bw2010 is calibrated on snippets; {granularity}-level vectors are "
|
|
126
|
+
"extrapolation beyond the model's native unit (BW-ALL-0002)",
|
|
127
|
+
ruling="BW-ALL-0002",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
lo, hi = CALIBRATED_LINE_RANGE
|
|
131
|
+
if granularity == "snippet" and not (lo <= len(lines) <= hi):
|
|
132
|
+
diags.append(
|
|
133
|
+
Diagnostic(
|
|
134
|
+
"info", "snippet-out-of-calibrated-range",
|
|
135
|
+
f"snippet is {len(lines)} lines; the bw2010 calibrated regime is "
|
|
136
|
+
f"{lo}-{hi} lines (BW-ALL-0002)",
|
|
137
|
+
ruling="BW-ALL-0002",
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
if scaffolded:
|
|
141
|
+
diags.append(
|
|
142
|
+
Diagnostic(
|
|
143
|
+
"info", "snippet-scaffolded",
|
|
144
|
+
"bare Java snippet was parsed inside a synthetic class/method "
|
|
145
|
+
"scaffold; features computed over the original lines only "
|
|
146
|
+
"(CORE-JAVA-0001)",
|
|
147
|
+
ruling="CORE-JAVA-0001",
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
return FeatureVectorResult(
|
|
151
|
+
feature_set="bw2010",
|
|
152
|
+
granularity=granularity,
|
|
153
|
+
native_granularity="snippet",
|
|
154
|
+
extrapolated=extrapolated,
|
|
155
|
+
names=BW_FEATURE_NAMES,
|
|
156
|
+
values=tuple(features[n] for n in BW_FEATURE_NAMES),
|
|
157
|
+
rulings=_bw_vector_rulings(adapter, lexical_fallback, cond_rulings),
|
|
158
|
+
diagnostics=tuple(diags),
|
|
159
|
+
unit_name=unit_name,
|
|
160
|
+
span=span,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cond_in_range(cond_lines: dict[str, set[int]], lo: int, hi: int) -> tuple[str, ...]:
|
|
165
|
+
"""Conditional tokenization rulings that engaged on a line within [lo, hi]."""
|
|
166
|
+
return tuple(sorted(
|
|
167
|
+
rid for rid, lns in cond_lines.items() if any(lo <= ln <= hi for ln in lns)
|
|
168
|
+
))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def function_vectors(
|
|
172
|
+
lines: list[str],
|
|
173
|
+
tokens: list[LexicalToken],
|
|
174
|
+
adapter: LanguageAdapter,
|
|
175
|
+
units: list[FunctionUnit],
|
|
176
|
+
opaque_tokens: list[LexicalToken] | None = None,
|
|
177
|
+
cond_opaque: dict[str, set[int]] | None = None,
|
|
178
|
+
cond_full: dict[str, set[int]] | None = None,
|
|
179
|
+
) -> list[FeatureVectorResult]:
|
|
180
|
+
"""Per-unit vectors. ``opaque_tokens`` is the error-opaque stream, passed
|
|
181
|
+
only when the file-level BW lexical fallback engaged (BW-ALL-0007): a
|
|
182
|
+
unit's vector is flagged only when its OWN span gained ERROR-region tokens
|
|
183
|
+
— the opaque stream is a subsequence of the full one, so equal slice
|
|
184
|
+
lengths imply identical slices. ``cond_opaque``/``cond_full`` map each
|
|
185
|
+
conditional tokenization ruling to the lines it engaged on in the
|
|
186
|
+
respective stream; a unit cites one only when it engaged inside the unit's
|
|
187
|
+
OWN span (in the stream that unit actually used)."""
|
|
188
|
+
out = []
|
|
189
|
+
for unit in units:
|
|
190
|
+
s, e = unit.span.start_line, unit.span.end_line
|
|
191
|
+
unit_tokens = rebase_tokens(tokens, s, e)
|
|
192
|
+
unit_fallback = (
|
|
193
|
+
opaque_tokens is not None
|
|
194
|
+
and len(unit_tokens) != len(rebase_tokens(opaque_tokens, s, e))
|
|
195
|
+
)
|
|
196
|
+
cond_src = cond_full if unit_fallback else cond_opaque
|
|
197
|
+
cond_rulings = cond_in_range(cond_src, s, e) if cond_src is not None else ()
|
|
198
|
+
out.append(
|
|
199
|
+
vector(
|
|
200
|
+
lines[s - 1 : e],
|
|
201
|
+
unit_tokens,
|
|
202
|
+
adapter,
|
|
203
|
+
"function",
|
|
204
|
+
unit_name=unit.qualified_name,
|
|
205
|
+
span=unit.span,
|
|
206
|
+
lexical_fallback=unit_fallback,
|
|
207
|
+
cond_rulings=cond_rulings,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
return out
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Retraining scaffold (ARCHITECTURE.md §7.4): feature extraction is decoupled from any
|
|
2
|
+
trained model; codecaliper ships NO weights. Model artifacts are JSON (never
|
|
3
|
+
pickle) and carry honesty metadata — applying a Java-snippet model elsewhere is
|
|
4
|
+
a construct-validity decision the user makes with eyes open.
|
|
5
|
+
|
|
6
|
+
The heavy lifting (logistic regression) needs the [retrain] extra
|
|
7
|
+
(scikit-learn); this module only defines the artifact contract so the §6.3
|
|
8
|
+
faithfulness pipeline and user retraining share one format.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import asdict, dataclass
|
|
15
|
+
|
|
16
|
+
from codecaliper.errors import SpecError
|
|
17
|
+
from codecaliper.readability.bw2010 import BW_FEATURE_ORDER_SHA
|
|
18
|
+
from codecaliper.spec import spec_version
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ModelArtifact:
|
|
23
|
+
feature_set: str # "bw2010"
|
|
24
|
+
feature_order_sha: str # must equal BW_FEATURE_ORDER_SHA at predict time
|
|
25
|
+
spec_version: str
|
|
26
|
+
trained_granularity: str
|
|
27
|
+
trained_language: str
|
|
28
|
+
dataset_id: str # e.g. "bw2010-original-100java" — an ID, never bundled data
|
|
29
|
+
protocol: str # e.g. "bw2010-logistic-10fold"
|
|
30
|
+
seed: int
|
|
31
|
+
coefficients: tuple[float, ...]
|
|
32
|
+
intercept: float
|
|
33
|
+
|
|
34
|
+
def save(self, path: str) -> None:
|
|
35
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump(asdict(self), f, indent=2, sort_keys=True)
|
|
37
|
+
f.write("\n")
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def load(cls, path: str) -> ModelArtifact:
|
|
41
|
+
with open(path, encoding="utf-8") as f:
|
|
42
|
+
data = json.load(f)
|
|
43
|
+
data["coefficients"] = tuple(data["coefficients"])
|
|
44
|
+
art = cls(**data)
|
|
45
|
+
if art.feature_set == "bw2010" and art.feature_order_sha != BW_FEATURE_ORDER_SHA:
|
|
46
|
+
raise SpecError(
|
|
47
|
+
"model artifact feature order does not match this codecaliper's "
|
|
48
|
+
"canonical bw2010 order — refusing to predict with misaligned features"
|
|
49
|
+
)
|
|
50
|
+
return art
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def new_artifact_metadata(
|
|
54
|
+
*, trained_granularity: str, trained_language: str, dataset_id: str,
|
|
55
|
+
protocol: str, seed: int,
|
|
56
|
+
) -> dict[str, object]:
|
|
57
|
+
"""Metadata stamp for a fresh training run (used by validation/bw_faithfulness)."""
|
|
58
|
+
return {
|
|
59
|
+
"feature_set": "bw2010",
|
|
60
|
+
"feature_order_sha": BW_FEATURE_ORDER_SHA,
|
|
61
|
+
"spec_version": spec_version(),
|
|
62
|
+
"trained_granularity": trained_granularity,
|
|
63
|
+
"trained_language": trained_language,
|
|
64
|
+
"dataset_id": dataset_id,
|
|
65
|
+
"protocol": protocol,
|
|
66
|
+
"seed": seed,
|
|
67
|
+
}
|