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
9 changes: 6 additions & 3 deletions docs/refraction_engine_differential.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,15 @@ fed to dead-code masking (`extract_lineage`, the slicer, the schema forge).
- **Same directory only.** Resolution looks only in the program's own directory, so zopeneditor's `COPYBOOK/` and CBSA's `cobol_copy/` are never found.
- **Sequence-number fields.** The patterns require blanks before `COPY`, so `R2 COPY SAM2PARM.` is missed. The DB's 2 zopeneditor edges are exactly these lines.
- **`EXEC SQL INCLUDE` ignored.** The DB's 15 extra CBSA edges are these (`ACCDB2` ×8, `PROCDB2` ×6, `CONTDB2` ×1).
- **Engine defect: ambiguous targets are dropped.** An ambiguous target produces no edge at all. This drops all 12 zopeneditor COPYs, and 36 CBSA COPYs whose copybook does exist in `cobol_copy/`:
- **Engine defect: ambiguous targets are dropped (fixed by #3199).** An ambiguous target produced no edge at all. This dropped all 12 zopeneditor COPYs, and 36 CBSA COPYs whose copybook does exist in `cobol_copy/`:
- zopeneditor ships each copybook twice (`COPYBOOK/` and `multiroot/copybooks/`).
- CBSA's `ACCTCTRL` stem matches `.cbl`, `.cpy`, `.jcl` and `.lked`.
- CBSA's `CUSTOMER` stem matches `CUSTOMER.cpy` and `CUSTOMER.java`.

A COPY target should prefer a copybook in the same language, then the nearest path.
A COPY target now prefers a copybook in the same language, then one that is not itself a
program (a PROGRAM-ID disqualifies the `.cbl`), then the nearest path; a remaining tie still
draws nothing. Engine `copybook paths` went from P 78/78 · R 78/114 to P 114/114 · R 114/114
on CBSA and from P 2/2 · R 2/14 to P 14/14 · R 14/14 on zopeneditor.
- **Semantic difference.**
- 11 CBSA names are system-supplied (CICS `DFHAID` ×9 and `DFHBMSCA`, Language Environment `CEEIGZCT`) and absent from the repo; unresolvable by construction.
- 9 CBSA names are BMS symbolic-map copybooks (`BNK1CAM` …), generated from `bms_src/*.bms` at build time. That edge belongs to the Phase-2 BMS work (#3122).
Expand All @@ -149,7 +152,7 @@ Dead code and lineage can move to the DB only when the engine carries them. File
|---|---|---|
| #3197 | engine | Area-B continuation lines become paragraphs (D1) |
| #3198 | engine | `usage_status`: entry flagged, case-sensitive, `NAME-EXIT` counts as a reference (D2) |
| #3199 | engine | resolver drops ambiguous COPY targets (D3) |
| #3199 | engine | resolver drops ambiguous COPY targets (D3) — **fixed**; both corpora now score `copybook paths` exact |
| #3200 | engine | no CALL / CICS LINK / JCL `EXEC PGM=` edges; unresolved CALLs unrecorded — **fixed** |
| #3201 | engine | no named SELECT/ASSIGN / OPEN-mode / DD extraction — **fixed** |
| #3202 | engine | `calls_out_to` is meaningless for COBOL |
Expand Down
2 changes: 1 addition & 1 deletion gitgalaxy/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ The counted rules say *that* a COBOL program calls out and touches files (`ipc_r

Three properties worth knowing before extending either:
* **A language opts in with a top-level `boundary_extraction` declaration**, never a key inside `rules` — `language_lens.py` re.compile()s every string value in `rules` (#2806).
* **Resolution is by PROGRAM-ID, nearest-wins.** That is deliberately *not* `network_risk_sensor.py`'s rule, which refuses to guess on an ambiguous stem (#3199): an import names a file, a called program is chosen by library concatenation order.
* **Resolution is by PROGRAM-ID, nearest-wins.** That is deliberately *not* `network_risk_sensor.py`'s rule: an import names a file, a called program is chosen by library concatenation order. Both share one proximity definition (`path_proximity.py`), and they disagree about what a tie means — a call picks one, an import that is still ambiguous draws nothing (#3199). The import resolver additionally disqualifies a candidate that declares a PROGRAM-ID, which is exactly the file a `CALL` wants.
* **These edges never enter the DAG.** PageRank, popularity, blast radius and every risk score are unaffected; only `edge_data` grows, under its own `edge_kind`.

### 6. `spatial_mapper.py` (The Positioning Engine)
Expand Down
60 changes: 17 additions & 43 deletions gitgalaxy/core/invocation_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
# WHY NOT `NetworkRiskSensor._resolve_target`:
# Two different relations with two different truths.
# - An IMPORT names a FILE (`COPY CUSTCOPY` -> CUSTCOPY.cpy), so the import
# resolver matches on path and stem, and REFUSES to guess when a stem is
# ambiguous (#3199) -- a wrong copybook edge is a wrong dependency.
# resolver matches on path and stem, and a candidate that declares a
# PROGRAM-ID is DISQUALIFIED: a copybook is a fragment, never a program
# (#3199).
# - A CALL names a PROGRAM (`CALL 'SAM2'` -> whichever file declares
# PROGRAM-ID SAM2), which is the class_data name, not the filename. Those
# usually coincide and sometimes do not, and a stem lookup cannot tell a
Expand All @@ -20,8 +21,9 @@
# several files (zopeneditor's COBOL/SAM2.cbl and multiroot/sam/SAM2.cbl) the
# NEAREST one wins -- the reading the answer key records, and the one that
# matches how the target is really chosen, by library concatenation order at
# link-edit or CICS-install time. The import resolver's refuse-to-guess rule is
# right for its relation and wrong for this one.
# link-edit or CICS-install time. Both resolvers now break a tie on proximity
# (core/path_proximity.py), but they still disagree about what a tie MEANS: a
# call picks one, an import that is still ambiguous draws nothing.
#
# THE EDGES ARE A SEPARATE KIND AND DO NOT ENTER THE GRAPH.
# `edge_kind` is 'call'/'exec', never 'import'. They are NOT handed to the
Expand All @@ -35,13 +37,20 @@
# `WHERE edge_kind = 'import'`, which is what tests/tools_recorders/
# test_edge_data.py asserts.
# ==============================================================================
from typing import Any, Optional
from typing import Any

from gitgalaxy.core.path_proximity import nearest_path

# Languages whose `classes` entries are program declarations a call can target.
# cobol only today: a JCL `EXEC PGM=` and a COBOL `CALL` both name a COBOL
# program. pli/rexx declare callable units too, but neither is a documented
# target of these verbs yet, so widening this is a measured change, not a guess.
_PROGRAM_LANGUAGES = ("cobol",)
#
# #3199 reads the same fact from the other side: if a `classes` entry here is a
# whole program, then a candidate file that has one is not a copybook, which is
# how `COPY ACCTCTRL` tells ACCTCTRL.cpy from the ACCTCTRL.cbl beside it. Adding
# a language here therefore has to be right for both readings.
PROGRAM_DECLARING_LANGUAGES = ("cobol",)

# `CALL`/`LINK`/`XCTL` are COBOL-side invocations; `EXEC PGM` is JCL's.
_EXEC_VERBS = ("EXEC PGM",)
Expand All @@ -51,7 +60,7 @@ def _program_index(parsed_files: list[dict[str, Any]]) -> dict[str, list[str]]:
"""PROGRAM-ID (upper-cased) -> the paths declaring it, in scan order."""
index: dict[str, list[str]] = {}
for f in parsed_files:
if str(f.get("lang_id", "")).lower() not in _PROGRAM_LANGUAGES:
if str(f.get("lang_id", "")).lower() not in PROGRAM_DECLARING_LANGUAGES:
continue
for cls in f.get("classes", []) or []:
name = str(cls.get("name", "")).strip().upper()
Expand All @@ -60,41 +69,6 @@ def _program_index(parsed_files: list[dict[str, Any]]) -> dict[str, list[str]]:
return index


def _nearest(candidates: list[str], src_path: str) -> Optional[str]:
"""The candidate sharing the longest directory prefix with `src_path`.

Ties break on the shallower path and then alphabetically, so the choice is
deterministic and independent of scan order -- a repository scanned on two
machines must produce the same edge.

EVERY comparison normalises separators first. The engine stores OS-native
paths, so on Windows an un-normalised depth count (`c.count("/")`) is 0 for
every candidate and the tiebreak silently degrades to alphabetical -- a
resolution that disagrees with Linux for the same repository. #3223 went red
on exactly this class of thing.
"""
if not candidates:
return None
if len(candidates) == 1:
return candidates[0]

def _posix(path: str) -> str:
return path.replace("\\", "/")

src_dirs = _posix(src_path).split("/")[:-1]

def _shared(candidate: str) -> int:
dirs = _posix(candidate).split("/")[:-1]
depth = 0
for a, b in zip(src_dirs, dirs):
if a != b:
break
depth += 1
return depth

return sorted(candidates, key=lambda c: (-_shared(c), _posix(c).count("/"), _posix(c)))[0]


def resolve_invocations(
parsed_files: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
Expand All @@ -120,7 +94,7 @@ def resolve_invocations(
target = site.get("target")
resolved = None
if target:
resolved = _nearest(index.get(str(target).upper(), []), src_path)
resolved = nearest_path(index.get(str(target).upper(), []), src_path)
# A program calling itself is recursion, not an edge: the
# import graph drops self-edges for the same reason.
if resolved == src_path:
Expand Down
Loading
Loading