Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,7 @@ def splice(

orphan_count = 0
duplicate_count = 0
orphan_names: list[str] = []
func_names = [f.get("name", "") for f in functions]
func_name_counts = collections.Counter(func_names)

Expand Down Expand Up @@ -1383,10 +1384,40 @@ def splice(
elif len(func_name) > 3 and token_counts[func_name] <= 1:
# If the function name only exists where it was defined, it's an orphan
orphan_count += 1
orphan_names.append(func_name)
usage_status = 1 # 1 = Orphan / Unused

func["usage_status"] = usage_status

# --- #2731: WHICH ORPHANS DID THE api RULE ALREADY COUNT? ---
# galaxyscope.py's Contextual Baseline Fix converts an imported
# file's orphans into API exposure. A function that is BOTH declared
# public AND uncalled was counted twice by that conversion -- once by
# the language's own `api` rule at its declaration, once as a
# converted orphan -- which is the common shape in library code,
# where the exported functions are exactly the ones with no in-repo
# caller. Count the overlap here (the orchestrator has no code text)
# and let the conversion credit only the remainder.
#
# The test is by NAME, not by span: an orphan's name occurs exactly
# once in the whole file (that is what the census above just proved),
# so a name appearing on a line the api rule matched can only be its
# own declaration -- no false positives are possible. Span
# containment would be both looser and tighter than that: looser
# because a long body can hold an unrelated api hit (C matches every
# non-static local declaration), tighter because the marker can sit
# outside the slicer's own span (JS/TS `export` precedes start_idx,
# php's span starts a line early, a java `@Test` line pulls start_line
# a line back off the `public` one).
api_declared_orphans = 0
if orphan_names and threat_locations.get("api"):
code_lines = code_stream.splitlines()
api_line_tokens: set[str] = set()
for line_no in set(threat_locations["api"]):
if 0 < line_no <= len(code_lines):
api_line_tokens.update(re.findall(r"\b\w+\b", code_lines[line_no - 1]))
api_declared_orphans = sum(1 for name in orphan_names if name in api_line_tokens)

if orphan_count > 0:
equations["orphaned_logic"] = orphan_count
if duplicate_count > 0:
Expand Down Expand Up @@ -1429,6 +1460,10 @@ def splice(
round((file_token_mass / 1000000) * 3.00, 5) if file_token_mass is not None else None
),
"threat_locations": threat_locations,
# #2731: how many of `orphaned_logic`'s functions the `api` rule
# already counted as public surface. Consumed by galaxyscope.py's
# Contextual Baseline Fix; never a signal in its own right.
"api_declared_orphans": api_declared_orphans,
}
if profile_regex:
result_payload["regex_telemetry"] = regex_telemetry
Expand Down
27 changes: 26 additions & 1 deletion gitgalaxy/galaxyscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2227,9 +2227,34 @@ def _calculate_risk_exposures(self):
if popularity > 0 and "equations" in meta:
orphans = meta["equations"].get("orphaned_logic", 0)
if orphans > 0:
# #2731: credit only the orphans the language's own `api`
# rule did NOT already count. A function that is both
# declared public and uncalled used to contribute twice --
# keyword-rosetta's `data/go/a.go` has three exported,
# uncalled functions and recorded raw_arch_api 3 + orphans 3
# = api 6, three functions for six units of public surface.
# The overlap is the common case, not the exception: a
# library's public functions are exactly the ones with no
# in-repo caller, so `risk_api_exposure` and
# `risk_documentation` (both read the adjusted `api`) were
# inflated most for the most library-shaped code.
#
# detector.py owns the overlap count -- it is a per-function
# name test against the api rule's own match lines, and the
# code text it needs is gone by the time we get here. A
# file-level `orphans - raw_api` subtraction would be wrong:
# a file's api hits also land on public classes, fields and
# grouped constants that are not functions at all, and would
# erase orphans the rule never saw.
already_public = min(meta.get("api_declared_orphans", 0), orphans)

# 1. Convert the dead weight into API Exposure
meta["equations"]["api"] = meta["equations"].get("api", 0) + orphans
meta["equations"]["api"] = meta["equations"].get("api", 0) + (orphans - already_public)
# 2. Wipe the Technical Debt
# Unconditional, and independent of the credit above: the
# file is imported, so none of its orphans are dead weight
# -- the already-public ones are simply surface the api
# rule had counted already.
meta["equations"]["orphaned_logic"] = 0

# 3. Heal the function metadata
Expand Down
81 changes: 81 additions & 0 deletions tests/core_engine/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3828,3 +3828,84 @@ def test_detector_yaml_single_line_step_survives_two_line_floor():
names = [f["name"] for f in result["functions"]]

assert len(names) == 2, f"expected both the single-line and multi-line run: steps, got {names}"


# ==============================================================================
# TEST: WHICH ORPHANS THE api RULE ALREADY COUNTED (#2731)
# ==============================================================================
def test_detector_counts_orphans_the_api_rule_already_declared():
"""
Regression for #2731: galaxyscope.py's Contextual Baseline Fix converts an
imported file's orphans into API exposure, but had no way to ask whether the
language's own `api` rule had already counted those same declarations -- so
a function that is both declared public and uncalled was counted twice
(keyword-rosetta's `data/go/a.go`: 3 exported, uncalled functions, api 6).

`api_declared_orphans` is that missing number. Uses the REAL definitions:
the whole point is the interaction with a language's actual api rule.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

go_detector = StructuralExtractor("go", LANGUAGE_DEFINITIONS)
code = (
"package main\n"
"\n"
"func ProbeGlobals(env int) int {\n"
" os.Getenv(env)\n"
" return env\n"
"}\n"
"\n"
"func ProbeSafety(value int) int {\n"
" context.Context(value)\n"
" return value\n"
"}\n"
)

result = go_detector.splice(code, "")

assert result["equations"].get("orphaned_logic", 0) == 2, "both exported functions must census as orphans"
assert result["equations"].get("api", 0) >= 2, "go's api rule must count both exported declarations"
assert result["api_declared_orphans"] == 2, (
"both orphans are declared public -- converting them again would double-count the same identifiers"
)


def test_detector_api_declared_orphans_ignores_hits_outside_the_declaration():
"""
#2731's overlap test is by NAME, not by span: an api hit that is not on the
orphan's own declaration line is somebody else's public surface and must not
suppress that orphan's conversion.

C is the sharp case. Its api rule matches any non-`static` declaration-shaped
line, including the local variable declarations inside a function body, so a
span-containment test would call every orphan already-public. A `static`
(file-local) function is not public surface at all: its conversion is the
Contextual Baseline Fix's own business, and #2731 must leave it alone.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

c_detector = StructuralExtractor("c", LANGUAGE_DEFINITIONS)
code = (
"static void hidden_helper(void)\n"
"{\n"
" int counter = 0;\n"
" counter++;\n"
"}\n"
"\n"
"void exported_entry(void)\n"
"{\n"
" int scratch = 0;\n"
" scratch++;\n"
"}\n"
)

result = c_detector.splice(code, "")

orphans = {f["name"] for f in result["functions"] if f.get("usage_status") == 1}
assert orphans == {"hidden_helper", "exported_entry"}, (
f"expected both functions to census as orphans, got {orphans}"
)
assert result["api_declared_orphans"] == 1, (
"only the non-static declaration is public surface the api rule already counted -- "
"the static one's body-local `int counter = 0;` api hit must not suppress it"
)
97 changes: 97 additions & 0 deletions tests/core_engine/test_galaxyscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,103 @@ def test_contextual_baseline_fix_snapshots_raw_signals(self):
self.assertEqual(island["equations"]["api"], 1)
self.assertEqual(island["equations"]["orphaned_logic"], 4)

# ==============================================================================
# TEST 17c: #2731 -- THE CONTEXTUAL BASELINE FIX CREDITS ONLY NEW SURFACE
# ==============================================================================
def test_contextual_baseline_fix_skips_already_declared_orphans(self):
"""
#2731: a function that is both declared public and uncalled used to be
counted twice by the Contextual Baseline Fix -- once by the language's
own api rule at its declaration, once as a converted orphan (go: three
exported, uncalled functions recorded api 6). Only the orphans the api
rule did NOT already count are new public surface.

detector.py supplies the overlap as meta["api_declared_orphans"]; the
conversion has to subtract it, and has to stay clamped -- the two numbers
are counted in different passes over the file.
"""
scope = Orchestrator(".", self.mock_config)

scope.ram_cache = {
# go/a.go's shape: 3 exported functions, none called in-file, all 3
# already counted by the api rule. Nothing new to credit.
"src/all_exported.go": {
"path": "src/all_exported.go",
"coding_loc": 100,
"lang_id": "go",
"equations": {"api": 3, "orphaned_logic": 3},
"api_declared_orphans": 3,
},
# Mixed: 3 orphans, 1 of them already public -> credit the other 2.
"src/mixed.go": {
"path": "src/mixed.go",
"coding_loc": 100,
"lang_id": "go",
"equations": {"api": 1, "orphaned_logic": 3},
"api_declared_orphans": 1,
},
# No overlap reported (the pre-#2731 shape, and every language whose
# api rule marks something other than function declarations): the
# original conversion is unchanged.
"src/no_overlap.py": {
"path": "src/no_overlap.py",
"coding_loc": 100,
"lang_id": "python",
"equations": {"api": 2, "orphaned_logic": 3},
},
# Defensive: an overlap larger than the orphan count must not
# subtract public surface the api rule genuinely measured.
"src/overclaimed.go": {
"path": "src/overclaimed.go",
"coding_loc": 100,
"lang_id": "go",
"equations": {"api": 4, "orphaned_logic": 2},
"api_declared_orphans": 5,
},
}
scope.popularity_scores = dict.fromkeys(scope.ram_cache, 2)

scope._calculate_risk_exposures()
by_path = {f.get("path"): f for f in scope.parsed_files}

self.assertEqual(
by_path["src/all_exported.go"]["equations"]["api"],
3,
"3 exported, uncalled functions must record 3 units of public surface, not 6!",
)
self.assertEqual(
by_path["src/mixed.go"]["equations"]["api"], 3, "only the un-declared orphans are new surface!"
)
self.assertEqual(
by_path["src/no_overlap.py"]["equations"]["api"],
5,
"a file with no reported overlap must convert as before!",
)
self.assertEqual(
by_path["src/overclaimed.go"]["equations"]["api"],
4,
"the overlap subtraction is clamped to the orphan count!",
)

# The debt wipe is unconditional either way: the file is imported, so
# none of its orphans are dead weight -- an already-declared orphan is
# surface the api rule had counted already, not debt. #2536's raw
# snapshot still carries the pre-adjustment counts.
for path, raw_orphans in (
("src/all_exported.go", 3),
("src/mixed.go", 3),
("src/no_overlap.py", 3),
("src/overclaimed.go", 2),
):
self.assertEqual(
by_path[path]["equations"]["orphaned_logic"], 0, f"{path}: imported file kept its orphan debt!"
)
self.assertEqual(
by_path[path]["raw_pre_adjustment"]["orphaned_logic"],
raw_orphans,
f"{path}: #2536's raw snapshot must still hold the pre-adjustment orphan count",
)

# ==============================================================================
# TEST 18: WORKER I/O ERRORS & BINARY THREAT ESCALATION
# ==============================================================================
Expand Down
Loading
Loading