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
29 changes: 29 additions & 0 deletions gitgalaxy/galaxyscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def missing_dependencies() -> dict[str, bool]:
# existing FileNotFoundError/CalledProcessError handling at each call site
# already covers that case unchanged.
_GIT_BIN = shutil.which("git") or "git"
# #3506: a scan holding any of these gets the mainframe skeleton-completeness report.
_COMPLETENESS_LANGUAGES = frozenset({"cobol", "jcl", "bms", "csd"})

logger = logging.getLogger("GalaxyScope")

Expand Down Expand Up @@ -1612,6 +1614,7 @@ def _box(text: str = "") -> None:
or self.config.get("SBOM_ONLY")
)
audit_output = "Skipped"
mainframe_completeness = None

# ==========================================================
# PHASE 12: ARCHIVAL & EXPORT ROUTING
Expand Down Expand Up @@ -1647,6 +1650,17 @@ def _box(text: str = "") -> None:
f"SQLITE_FAILURE: Could not generate native database. {e}",
exc_info=True,
)
else:
# #3506: the mainframe skeleton's completeness, read back from the
# DB just written, for the audit report and the LLM brief.
try:
mainframe_completeness = self._mainframe_completeness(
repository_graph, db_output, session_meta.get("target")
)
except Exception as e:
logger.error(
f"COMPLETENESS_FAILURE: Could not score the mainframe skeleton. {e}", exc_info=True
)

# --- Phase 12.1: Audit Recorder (Forensic Log) ---
if not exclusive_mode or self.config.get("AUDIT_ONLY"):
Expand All @@ -1663,6 +1677,7 @@ def _box(text: str = "") -> None:
forensic_report=report,
session_meta=session_meta,
output_path=audit_output,
mainframe_completeness=mainframe_completeness, # #3506
)
except Exception as e:
logger.error(
Expand All @@ -1684,6 +1699,7 @@ def _box(text: str = "") -> None:
output_dir=output_dir,
forensic_report=report,
call_resolution=self.fcall_stats, # #3331
mainframe_completeness=mainframe_completeness, # #3506
)
except Exception as e:
logger.error(
Expand Down Expand Up @@ -3089,6 +3105,19 @@ def _render_splicing_chart(self):

print("=" * 75 + "\n")

@staticmethod
def _mainframe_completeness(repository_graph, db_path: str, repo_name: Optional[str]) -> Optional[dict]:
"""GalaxyIR.completeness() of the scan just recorded (#3506), or None.

Only a scan holding COBOL, JCL, BMS or CSD has a mainframe skeleton to
score; every other scan returns before the IR is imported or loaded.
"""
if not any(f.get("lang_id") in _COMPLETENESS_LANGUAGES for f in repository_graph or ()):
return None
from gitgalaxy.tools.cobol_to_cobol.galaxy_ir import load_galaxy_ir

return load_galaxy_ir(Path(db_path), repo_name).completeness()

def _get_git_audit(self) -> dict[str, str]:
"""
Extracts forensic Git metadata (Commit SHA, Branch, Remote URL, Date) via subprocess.
Expand Down
32 changes: 32 additions & 0 deletions gitgalaxy/recorders/audit_recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,33 @@ def descale(self, key: str, value: Any, default_scalar: float = 1.0) -> Any:
return round(value / default_scalar, 3)
return value

@staticmethod
def _completeness_block(report):
"""#3506: GalaxyIR.completeness() in the audit's labelled style -- the score,
each channel's resolved / total / system names and named gaps, and what to
ask the estate owner for (docs/mainframe_ingestion_checklist.md)."""

def pct(ratio):
return f"{ratio:.1%}" if ratio is not None else "N/A"

return {
"Score (Mean Channel Ratio)": pct(report["score"]),
"Channels": {
name: {
"Resolved": ch["resolved"],
"Total": ch["total"],
"Ratio": pct(ch["ratio"]),
"System Names (Not Gaps)": ch["system"],
"Gaps": ch["gaps"],
}
for name, ch in report["channels"].items()
},
"Missing Inputs": [
{"Input": m["input"], "Gap Count": m["count"], "Examples": m["examples"]}
for m in report["missing_inputs"]
],
}

def _mainframe_facts_block(self, file_data):
"""The Named System Facts for one file (#3200/#3201/#3246/#3250/#3344/#3356), or {} if none.

Expand Down Expand Up @@ -504,10 +531,13 @@ def generate_report(
forensic_report,
session_meta,
output_path,
mainframe_completeness=None,
):
"""
Transforms raw pipeline state into a verbose forensic compliance manifest.
Memory-optimized to handle enterprise monorepos (10,000+ files) efficiently.
`mainframe_completeness` (#3506) is GalaxyIR.completeness() of the scan,
rendered as section 7 -- absent when None (every non-mainframe scan).
"""
# 1. Forensic Traceability Anchor
# Cryptographically binds this audit log to a specific moment in the source control history.
Expand Down Expand Up @@ -1049,6 +1079,8 @@ def generate_report(
"5. Unparsable Artifacts (Excluded Artifacts Queue)": pretty_unparsable,
"6. Parsed Files (Scanned Artifacts)": pretty_directory_groups,
}
if mainframe_completeness is not None:
mission_audit["7. Mainframe Skeleton Completeness"] = self._completeness_block(mainframe_completeness)

target_path = Path(output_path)

Expand Down
23 changes: 22 additions & 1 deletion gitgalaxy/recorders/llm_recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,14 @@ def generate_artifacts(
output_dir: str,
forensic_report: Optional[dict[str, Any]] = None,
call_resolution: Optional[dict[str, Any]] = None,
mainframe_completeness: Optional[dict[str, Any]] = None,
):
"""Generates the dual-output AI artifacts: Markdown and SQLite.

`call_resolution` (#3331) is the call resolver's stats, rendered as the
brief's function-call resolution section (absent when None/empty)."""
brief's function-call resolution section (absent when None/empty).
`mainframe_completeness` (#3506) is GalaxyIR.completeness(), one line at
the end of the mainframe facts section (absent when None)."""
if forensic_report is None:
forensic_report = {}

Expand Down Expand Up @@ -272,6 +275,7 @@ def generate_artifacts(
session_meta,
forensic_report,
call_resolution,
mainframe_completeness,
)

try:
Expand Down Expand Up @@ -456,6 +460,21 @@ def _lexicon_lines(self) -> list[str]:
lines.append("")
return lines

@staticmethod
def _completeness_lines(report: Optional[dict[str, Any]]) -> list[str]:
"""#3506: the mainframe skeleton's completeness score and its top three
missing inputs, in one line (the channel table is in the audit report)."""
if report is None:
return []
score = f"{report['score']:.0%}" if report["score"] is not None else "n/a (no channel has facts)"
top = sorted(report["missing_inputs"], key=lambda m: (-m["count"], m["input"]))[:3]
missing = "; ".join(f"{m['input']} ({m['count']} gaps)" for m in top) or "none"
return [
f"- **Mainframe skeleton completeness:** {score} (mean channel ratio; channel table in the audit "
f"report, section 7). Top missing inputs: {missing}.",
"",
]

def _call_resolution_lines(self, call_resolution: Optional[dict[str, Any]]) -> list[str]:
"""#3331: how many function calls the resolver linked, and how surely.

Expand Down Expand Up @@ -949,6 +968,7 @@ def _build_markdown(
session_meta: dict[str, Any],
forensic_report: dict[str, Any],
call_resolution: Optional[dict[str, Any]] = None,
mainframe_completeness: Optional[dict[str, Any]] = None,
) -> str:
"""Constructs a high-density, context-rich Markdown brief for LLM agents."""
target = session_meta.get("target", "Project")
Expand Down Expand Up @@ -2010,6 +2030,7 @@ def _outbound(file_data):
# is absent from every non-mainframe brief.
# ==============================================================================
lines.extend(self._mainframe_facts_lines(parsed_files))
lines.extend(self._completeness_lines(mainframe_completeness))

# --- 14. PROJECT IDIOM WRAPPERS (#3313 step 3) ---
# Optional: renders only when the scan resolved at least one wrapper.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading