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
108 changes: 101 additions & 7 deletions gitgalaxy/core/network_risk_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
from typing import Any, Optional

from gitgalaxy.standards.analysis_lens import RECORDING_SCHEMAS
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

# #2540: languages whose import/module references resolve case-insensitively
# (fortran `USE A` binds a.f90, cobol `COPY A.` binds a.cpy, haskell's
# necessarily-capitalized `import A` may live in a.hs). Declared per-language
# via the "case_insensitive_imports" flag on each language DEFINITION;
# resolution for every other language stays strictly case-sensitive.
CASE_INSENSITIVE_IMPORT_LANGS = frozenset(
lang_id for lang_id, definition in LANGUAGE_DEFINITIONS.items() if definition.get("case_insensitive_imports")
)

HAS_NETWORKX = False
try:
Expand Down Expand Up @@ -57,12 +67,61 @@ def _build_resolution_map(self, files: list[dict[str, Any]]) -> dict[str, list[s

return resolution_map

def _resolve_target(self, target_token: str, resolution_map: dict[str, list[str]], curr_path: str) -> Optional[str]:
def _build_folded_resolution_map(self, files: list[dict[str, Any]]) -> dict[str, dict[str, list[str]]]:
"""
#2540: derives per-language lowercase-keyed views of the resolution
keys so imports from case-insensitive-resolution languages (fortran,
cobol, haskell — see CASE_INSENSITIVE_IMPORT_LANGS) can fall back to a
case-folded lookup. Kept separate from the exact-case map so
case-sensitive languages never gain cross-case resolution, and so
exact-case matches always win first even for the folding languages.

Folding is a property of the *importing* language's resolution rules,
so each language's folded view holds only that language's own files —
haskell's `import Text.Pandoc.Generic` must not fold onto a go file
named generic.go just because the stems collide case-insensitively.
(The exact-case stages keep their long-standing language-agnostic
stem matching; only the folded fallback is scoped.)
"""
folded_maps: dict[str, dict[str, list[str]]] = {}
for f in files:
lang = str(f.get("lang_id", "")).lower()
if lang not in CASE_INSENSITIVE_IMPORT_LANGS:
continue
path = f.get("path", "")
if not path:
continue
lang_map = folded_maps.setdefault(lang, defaultdict(list))
name = f.get("name", Path(path).name)
stem = Path(path).stem
lang_map[path.lower()].append(path)
if name:
lang_map[name.lower()].append(path)
if stem:
lang_map[stem.lower()].append(path)
return folded_maps

def _resolve_target(
self,
target_token: str,
resolution_map: dict[str, list[str]],
curr_path: str,
folded_maps: Optional[dict[str, dict[str, list[str]]]] = None,
fold_lang: Optional[str] = None,
) -> Optional[str]:
"""
Resolves an import token to a single file path, refusing to guess when
genuinely ambiguous rather than silently misattributing an edge.

When `fold_lang` is set (the importing file's language resolves
imports case-insensitively, #2540) and no exact-case key matches, the
lookup retries against that language's own folded map with a
lowercased token. Exact matches always win first, so mixed-case repos
keep their precise edges.
"""
token_as_path = target_token.replace(".", "/").replace("\\", "/")
bare_component = token_as_path.rsplit("/", 1)[-1]
folded_hit = False

# Stage 1: direct key lookup — handles full-path, bare-filename, and
# bare-stem tokens that match a stored key exactly.
Expand All @@ -74,9 +133,21 @@ def _resolve_target(self, target_token: str, resolution_map: dict[str, list[str]
# token's final path component so there's something to disambiguate
# against in Stage 2.
if not candidates:
bare_component = token_as_path.rsplit("/", 1)[-1]
candidates = resolution_map.get(bare_component)

# Stage 1c (#2540): case-insensitive-resolution languages retry the
# same two lookups case-folded, only after both exact-case stages
# miss — fortran `USE A` -> a.f90, cobol `COPY A.` -> a.cpy,
# haskell `import A` -> a.hs. Scoped to the importing language's own
# files so folding never invents a cross-language edge.
if not candidates and fold_lang and folded_maps is not None:
lang_map = folded_maps.get(fold_lang)
if lang_map:
folded_hit = True
candidates = lang_map.get(target_token.lower())
if not candidates:
candidates = lang_map.get(bare_component.lower())

if not candidates:
return None

Expand All @@ -86,9 +157,17 @@ def _resolve_target(self, target_token: str, resolution_map: dict[str, list[str]

# Stage 2: multiple files share this name/stem — disambiguate using
# any path context already present in the token, comparing against
# each candidate's path with its extension stripped.
# each candidate's path with its extension stripped. A case-folded
# hit compares case-folded here too, for consistency with Stage 1c.
cmp_token = token_as_path.lower() if folded_hit else token_as_path
path_matches = [
c for c in candidates if str(Path(c).with_suffix("")).replace("\\", "/").endswith(token_as_path)
c
for c in candidates
if (
str(Path(c).with_suffix("")).replace("\\", "/").lower()
if folded_hit
else str(Path(c).with_suffix("")).replace("\\", "/")
).endswith(cmp_token)
]
if len(path_matches) == 1:
return path_matches[0]
Expand All @@ -100,6 +179,12 @@ def _resolve_target(self, target_token: str, resolution_map: dict[str, list[str]
)
return None

@staticmethod
def _fold_lang(f: dict[str, Any]) -> Optional[str]:
"""#2540: the file's language id if it resolves import targets case-insensitively, else None."""
lang = str(f.get("lang_id", "")).lower()
return lang if lang in CASE_INSENSITIVE_IMPORT_LANGS else None

def extract_test_coverage_mapping(self, files: list[dict[str, Any]]) -> dict[str, dict[str, list[dict[str, Any]]]]:
"""
Maps function calls from test files to their imported production targets.
Expand All @@ -111,6 +196,7 @@ def extract_test_coverage_mapping(self, files: list[dict[str, Any]]) -> dict[str
"""
coverage_map: dict[str, dict[str, list[dict[str, Any]]]] = {}
resolution_map = self._build_resolution_map(files)
folded_maps = self._build_folded_resolution_map(files)

# 2. Identify Test Files and extract their outgoing invocations
for f in files:
Expand All @@ -126,7 +212,9 @@ def extract_test_coverage_mapping(self, files: list[dict[str, Any]]) -> dict[str
target_paths = set()
for imp in f.get("raw_imports", []):
target_token = imp[0] if isinstance(imp, tuple) and len(imp) == 2 else imp
target_path = self._resolve_target(target_token, resolution_map, path)
target_path = self._resolve_target(
target_token, resolution_map, path, folded_maps=folded_maps, fold_lang=self._fold_lang(f)
)

if target_path and target_path != path:
target_paths.add(target_path)
Expand Down Expand Up @@ -174,6 +262,7 @@ def build_dependency_graph(self, parsed_files: list[dict[str, Any]]) -> tuple[li

# 1. Build the Resolution Map (Fast Path Lookup)
resolution_map = self._build_resolution_map(parsed_files)
folded_maps = self._build_folded_resolution_map(parsed_files)
for f in parsed_files:
path = f.get("path", "")

Expand All @@ -196,7 +285,9 @@ def build_dependency_graph(self, parsed_files: list[dict[str, Any]]) -> tuple[li
target_token = imp
entity = None

target_path = self._resolve_target(target_token, resolution_map, curr_path)
target_path = self._resolve_target(
target_token, resolution_map, curr_path, folded_maps=folded_maps, fold_lang=self._fold_lang(f)
)
if target_path and target_path != curr_path:
# Edge weight can be increased if specific entities are highly coupled
weight = 1.5 if entity else 1.0
Expand Down Expand Up @@ -382,6 +473,7 @@ def _fallback_build_graph(self, parsed_files: list[dict[str, Any]]) -> tuple[lis
)

resolution_map = self._build_resolution_map(parsed_files)
folded_maps = self._build_folded_resolution_map(parsed_files)

in_degrees = {f.get("path", ""): 0 for f in parsed_files}
out_degrees = {f.get("path", ""): 0 for f in parsed_files}
Expand All @@ -390,7 +482,9 @@ def _fallback_build_graph(self, parsed_files: list[dict[str, Any]]) -> tuple[lis
curr_path = f.get("path", "")
for imp in f.get("raw_imports", []):
target_token = imp[0] if isinstance(imp, tuple) and len(imp) == 2 else imp
target_path = self._resolve_target(target_token, resolution_map, curr_path)
target_path = self._resolve_target(
target_token, resolution_map, curr_path, folded_maps=folded_maps, fold_lang=self._fold_lang(f)
)
if target_path and target_path != curr_path:
out_degrees[curr_path] = out_degrees.get(curr_path, 0) + 1
in_degrees[target_path] = in_degrees.get(target_path, 0) + 1
Expand Down
17 changes: 17 additions & 0 deletions gitgalaxy/galaxyscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,23 @@ def _calculate_risk_exposures(self):
meta["metadata"]["folder_dominant_lang"] = folder_dominant_langs.get(folder, meta.get("lang_id", "unknown"))
# -----------------------------------------------------------------

# =================================================================
# ---> #2536: RAW PRE-ADJUSTMENT SNAPSHOT <---
# The Contextual Baseline Fix below rewrites api/orphaned_logic
# IN PLACE for any imported file, which made the raw extraction
# counts unrecoverable from the recorder DB (the #1096
# cross-language control corpus needs them). Snapshot the affected
# keys unconditionally BEFORE the adjustment runs so files the
# adjustment never touches record raw == adjusted. Purely
# additive: scoring still consumes the adjusted equations.
# =================================================================
pre_adjust_eq = meta.get("equations", {})
meta["raw_pre_adjustment"] = {
"api": pre_adjust_eq.get("api", 0),
"orphaned_logic": pre_adjust_eq.get("orphaned_logic", 0),
}
# =================================================================

# =================================================================
# ---> THE CONTEXTUAL BASELINE FIX <---
# If the file is imported by the ecosystem, its "orphans" are actually its API.
Expand Down
45 changes: 45 additions & 0 deletions gitgalaxy/recorders/record_keeper.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ def record_mission(
appsec_god_mode BOOLEAN DEFAULT 0,
hallucination_zone BOOLEAN DEFAULT 0,
silent_mutation_risk BOOLEAN DEFAULT 0,
raw_arch_api INTEGER DEFAULT 0,
raw_state_slop_orphans INTEGER DEFAULT 0,
{", ".join(risk_cols)},
{", ".join(hit_cols)}
)
Expand Down Expand Up @@ -368,6 +370,29 @@ def record_mission(
else:
raise

# #2536: galaxyscope.py's Contextual Baseline Fix folds orphaned_logic
# into api for any imported file BEFORE the hit_vector is built, so
# arch_api / state_slop_orphans only ever carry the ADJUSTED values.
# These two columns persist the raw pre-adjustment extraction counts
# alongside them (needed by the #1096 cross-language control corpus);
# for files the adjustment never touches, raw == adjusted. Same
# auto-heal precedent as doc_loc above for pre-#2536 databases.
try:
cursor.execute("ALTER TABLE file_data ADD COLUMN raw_arch_api INTEGER DEFAULT 0")
except sqlite3.OperationalError as exc:
if "duplicate column name" in str(exc).lower():
self.logger.debug("Schema migration skipped: 'raw_arch_api' already exists.")
else:
raise

try:
cursor.execute("ALTER TABLE file_data ADD COLUMN raw_state_slop_orphans INTEGER DEFAULT 0")
except sqlite3.OperationalError as exc:
if "duplicate column name" in str(exc).lower():
self.logger.debug("Schema migration skipped: 'raw_state_slop_orphans' already exists.")
else:
raise

cursor.execute("""
CREATE TABLE IF NOT EXISTS excluded_artifacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
Expand Down Expand Up @@ -406,6 +431,13 @@ def record_mission(
# PERFORMANCE OPTIMIZATION: Global array for batched executemany inserts
all_func_rows = []

# #2536: hit_vector indexes for the two keys the Contextual Baseline
# Fix rewrites -- used to fall back to the adjusted values whenever a
# producer doesn't ship the raw snapshot (synthetic leak/model nodes),
# so raw == adjusted there rather than a fake zero.
api_sig_idx = self.SIGNAL_SCHEMA.index("api") if "api" in self.SIGNAL_SCHEMA else -1
orphan_sig_idx = self.SIGNAL_SCHEMA.index("orphaned_logic") if "orphaned_logic" in self.SIGNAL_SCHEMA else -1

for file_data in parsed_files:
tel = file_data.get("telemetry", {})
functions = file_data.get("functions", [])
Expand Down Expand Up @@ -465,6 +497,16 @@ def record_mission(
rv = file_data.get("risk_vector", [0.0] * len(self.RISK_SCHEMA))
hv = file_data.get("hit_vector", [0] * len(self.SIGNAL_SCHEMA))

# #2536: raw pre-adjustment extraction counts, snapshotted by
# galaxyscope.py BEFORE the Contextual Baseline Fix rewrites
# api/orphaned_logic. Absent snapshot -> fall back to the adjusted
# hit_vector slots (raw == adjusted).
raw_pre = file_data.get("raw_pre_adjustment", {})
adjusted_api = hv[api_sig_idx] if 0 <= api_sig_idx < len(hv) else 0
adjusted_orphans = hv[orphan_sig_idx] if 0 <= orphan_sig_idx < len(hv) else 0
raw_api = int(raw_pre.get("api", adjusted_api))
raw_orphans = int(raw_pre.get("orphaned_logic", adjusted_orphans))

file_archetype = tel.get("archetype", "Unknown")
file_fingerprint_str = json.dumps(tel.get("archetype_fingerprint", {}))

Expand Down Expand Up @@ -696,6 +738,8 @@ def record_mission(
god_mode,
hallucination_zone,
silent_mutation,
raw_api,
raw_orphans,
]

row_data.extend(rv)
Expand Down Expand Up @@ -723,6 +767,7 @@ def record_mission(
ecosystem_baseline, repo_z_score,
ai_threat_score, is_malware, has_credentials, binary_anomaly, obfuscation_flag,
token_mass, financial_read_cost, agentic_isolation_risk, requires_hitl, appsec_god_mode, hallucination_zone, silent_mutation_risk,
raw_arch_api, raw_state_slop_orphans,
{", ".join([f"risk_{r.replace('-', '_')}" for r in self.RISK_SCHEMA])},
{", ".join([self.SHORT_KEY_MAP.get(h, h) for h in self.SIGNAL_SCHEMA])}
) VALUES ({placeholders})
Expand Down
4 changes: 4 additions & 0 deletions gitgalaxy/standards/language_standards/languages/cobol.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
# Rationale: Strictly fixed-format. The engine must monitor Column 7 for an asterisk '*'
# or slash '/' to identify line-level Commented / Non-Executable Text.
"lexical_family": "positional_anchored",
# #2540: COBOL copybook names are case-insensitive (`COPY A.` binds the
# same copybook as `copy a.`), so the dependency DAG's import-token ->
# file lookup (network_risk_sensor.py) case-folds for this language.
"case_insensitive_imports": True,
"rules": {
# --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE ---
# 1. branch: Entscheidungslogik. Control flow that splits execution paths.
Expand Down
6 changes: 6 additions & 0 deletions gitgalaxy/standards/language_standards/languages/fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
# UPGRADED: Maps to Family 7 (The Positional Ancients)
# Rationale: Fixed-format requires Column 1 monitoring ('C' or '*'); Free-format uses '!'.
"lexical_family": "positional_anchored",
# #2540: Fortran resolves module names case-insensitively (`USE A` and
# `use a` both bind module a). The dependency DAG's import-token ->
# file lookup (network_risk_sensor.py) must case-fold for this
# language or uppercase legacy style (`USE A`) never resolves to
# `a.f90` and the whole import chain goes invisible.
"case_insensitive_imports": True,
"rules": {
# --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE ---
# 1. branch (Control Flow / Branching)
Expand Down
10 changes: 9 additions & 1 deletion gitgalaxy/standards/language_standards/languages/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,15 @@
),
# 7. safety_neg (Safety Bypasses / Unchecked Types)
# Explicitly ignoring errors via blank identifier.
"safety_bypasses": re.compile(r'_\s*,\s*err[ \t]*=|_[ \t]*=\s*\w+|\bimport\s+(?:\.[ \t]+)?"'),
# BUG FIX (#2542): the import alternation made the dot OPTIONAL
# (`import\s+(?:\.[ \t]+)?"`), so EVERY plain quoted import
# (`import "fmt"`) counted as a safety bypass, not just the
# namespace-polluting dot-import form (`import . "fmt"`). The dot
# is now mandatory. Note this alternation only ever matched the
# single-line form either way: grouped imports (`import (\n...\n)`)
# have a `(` between `import` and the quoted path, which the
# pattern never crossed before and still doesn't.
"safety_bypasses": re.compile(r'_\s*,\s*err[ \t]*=|_[ \t]*=\s*\w+|\bimport\s+\.[ \t]+"'),
# 8. danger (High-Risk Execution / System Calls)
# Process-killing commands and direct syscalls. EXCLUDES TODO (debt) and fmt.Print (print_hits).
"high_risk_execution": re.compile(r"\b(os\.Exit|syscall\.Kill|syscall\.RawSyscall|log\.Fatal(?:f|ln)?)\b"),
Expand Down
7 changes: 7 additions & 0 deletions gitgalaxy/standards/language_standards/languages/haskell.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
# (standard_block never used the `--`/`{-`/`-}` tokens).
# Rationale: Uses '--' for lines and '{- -}' for blocks, which strictly supports recursive nesting.
"lexical_family": "recursive_block_haskell",
# #2540: Haskell module names are necessarily capitalized (`import A`),
# while on-disk file names may not match that casing exactly -- exact-case
# lookup makes `import A` miss `a.hs` and the whole DAG for the repo goes
# invisible. The dependency DAG's import-token -> file lookup
# (network_risk_sensor.py) case-folds for this language; exact-case
# matches still win first.
"case_insensitive_imports": True,
"rules": {
# --- PHASE 1: LOGIC TOPOLOGY & STRUCTURE ---
# branch: decisions that split flow. Includes guards (|) and modern \cases.
Expand Down
Loading
Loading