mainframe-migration-toolkit 0.2.0__tar.gz → 0.3.1__tar.gz
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.
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/.claude/skills/analyze-mainframe-similarity/SKILL.md +3 -1
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/.claude/skills/migrate-mainframe-job/SKILL.md +1 -1
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/AGENTS.md +1 -2
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/PKG-INFO +1 -1
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/pyproject.toml +1 -1
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/__init__.py +3 -2
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/cli.py +83 -1
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/similarity.py +284 -27
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/synthetic.py +39 -1
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/.claude/skills/validate-golden-dataset/SKILL.md +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/.gitignore +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/CLAUDE.md +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/__main__.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/cobol.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/copybook.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/errors.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/external.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/io.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/jcl.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/pipeline.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/sequential.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/sorting.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/specs.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/workspace.py +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/.mvn/wrapper/maven-wrapper.properties +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/mvnw +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/mvnw.cmd +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/pom.xml +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/AvroValueFormatter.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/CsvTabularReader.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/DataFormat.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/Difference.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/InputFileSet.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/InputOptions.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/JsonReportWriter.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/MultiFileTabularReader.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/Normalization.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/ParquetTabularReader.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/TabularReader.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/TabularReaderFactory.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/ValidationOptions.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/ValidationReport.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/ValidationService.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/ValidatorCli.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/main/java/io/mainframe/migration/validator/ValueNormalizer.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/test/java/io/mainframe/migration/validator/DirectoryValidationTest.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/test/java/io/mainframe/migration/validator/JsonReportWriterTest.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/test/java/io/mainframe/migration/validator/KeyedValidationTest.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/test/java/io/mainframe/migration/validator/ParquetValidationTest.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/test/java/io/mainframe/migration/validator/ValidationServiceTest.java +0 -0
- {mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/src/test/java/io/mainframe/migration/validator/ValidatorCliTest.java +0 -0
|
@@ -8,11 +8,13 @@ description: Cluster COBOL and JCL source by deterministic textual similarity, i
|
|
|
8
8
|
1. Run the deterministic scan:
|
|
9
9
|
|
|
10
10
|
```text
|
|
11
|
-
python -m mainframe_toolkit similarity scan <source-root> --threshold <threshold> --json similarity.json --csv similarity.csv --review-request similarity-review-request.json
|
|
11
|
+
python -m mainframe_toolkit similarity scan <source-root> --algorithm advanced --threshold <threshold> --json similarity.json --csv similarity.csv --review-request similarity-review-request.json
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Use the requested threshold, or 80 when absent. Never edit deterministic scores or cluster membership manually.
|
|
15
15
|
|
|
16
|
+
Inspect each pair's `componentes` (`conteudo`, `ordem`, `renomeacao`, `estrutura`, `literais`, and `clone_parcial`). Also inspect `clones_parciais`, which are evidence of shared blocks but do not automatically join clusters. Use `--cluster-policy cohesive` when every pair in a cluster must meet the threshold; keep `connected` for broader codebase discovery.
|
|
17
|
+
|
|
16
18
|
2. For each non-singleton cluster in the review request, read every original file and its normalized evidence. Check all pair relationships; connected-component chaining alone is not proof of equivalence. Ignore comments, sequence columns, copyright blocks, whitespace, generated names, and environment-only identifiers. Compare JCL execution flow/DD semantics or COBOL inputs, outputs, branches, calculations, side effects, error behavior, and record layouts.
|
|
17
19
|
|
|
18
20
|
3. Assign exactly one verdict per requested cluster:
|
|
@@ -45,7 +45,7 @@ For each executable step in JCL order:
|
|
|
45
45
|
1. Implement only that program's behavior from its spec and source. Preserve COBOL decimal/truncation, key, mutation, EOF, return-code, and error semantics unless the specification explicitly overrides them.
|
|
46
46
|
2. Add its import and `PipelineProgram` entry to the JCL-named file immediately.
|
|
47
47
|
3. Run fast module tests, then execute the entire pipeline prefix with `--through <PROGRAM>` using supplied inputs.
|
|
48
|
-
4. If a required input, dataset, clock value, or external implementation is absent, create the smallest deterministic substitute that makes semantic sense.
|
|
48
|
+
4. If a required input, dataset, clock value, or external implementation is absent, create the smallest deterministic substitute that makes semantic sense. If a missing external function's arguments and result are observable in the golden dataset, run `python -m mainframe_toolkit synthetic-lookup <golden> <lookup.csv|lookup.parquet> --key <argument> [--key <argument>...] --value <result> --program <PROGRAM>` and make its adapter read the generated lookup. Reject conflicting results for the same arguments; investigate a missing argument instead of choosing a value. Use the golden only to build this unavailable-dependency lookup, never at pipeline runtime or as embedded business rules. Keep relationships valid, place substitutes under `converted/<JOB>/synthetic/`, and preserve the generated provenance manifest. Never overwrite or shadow supplied data.
|
|
49
49
|
5. Confirm the prefix has no import, analysis, Spark, schema, conversion, or runtime errors. Confirm each program output exists both in the in-memory registry and its audit path. Fix the current or earlier responsible module before moving to the next step.
|
|
50
50
|
|
|
51
51
|
Do not use the final golden comparison for a partial prefix unless that prefix is itself the documented final producer.
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
- Convert a job strictly in JCL execution order. After each program, import it from the JCL-named entrypoint and execute the complete prefix through that program. Fix every syntax or runtime failure before continuing.
|
|
5
5
|
- Keep intermediate datasets as PySpark DataFrames in `PipelineContext`; never use an audit file as the handoff between programs. Publish every program output so the runtime also materializes an audit copy.
|
|
6
6
|
- Put business rules only in program modules. The JCL-named entrypoint may initialize Spark/context, import programs, declare their order/conditions, parse runtime arguments, and run the pipeline.
|
|
7
|
-
- Generate deterministic synthetic data or an external-program adapter only for an unavailable dependency. Never replace supplied data. Preserve referential integrity and identify every synthetic resource in the final report.
|
|
7
|
+
- Generate deterministic synthetic data or an external-program adapter only for an unavailable dependency. When the missing dependency's arguments and result are observable in the golden dataset, reconstruct a deduplicated lookup keyed by those arguments, reject ambiguous mappings, and make the adapter read that lookup; never read the golden at runtime or encode its values as business rules. Never replace supplied data. Preserve referential integrity and identify every synthetic resource in the final report.
|
|
8
8
|
- Completion requires a full run and 100% cell-level equality with the golden dataset. Use explicit ordering keys when row order is not part of the contract. Trace mismatches to the responsible program, fix it, and rerun until equal.
|
|
9
9
|
- Do not add deployment, infrastructure, production hardening, architecture prose, README files, or per-step reports. Generate only functional core code, tests, audit artifacts, and the final `RELATORIO_FINAL.md` for each converted job.
|
|
10
|
-
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mainframe-migration-toolkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Runtime and deterministic tools for COBOL/JCL to PySpark migrations"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
authors = [{ name = "Mainframe Migration Toolkit" }]
|
|
@@ -37,7 +37,7 @@ from .pipeline import (
|
|
|
37
37
|
)
|
|
38
38
|
from .sequential import FieldSpec, RecordLayout, convert_sequential, iter_sequential
|
|
39
39
|
from .sorting import SortKey, sort_dataframe, sort_records
|
|
40
|
-
from .synthetic import synthetic_dataframe, synthetic_rows
|
|
40
|
+
from .synthetic import infer_lookup_rows, synthetic_dataframe, synthetic_rows
|
|
41
41
|
from .workspace import WorkspaceInitResult, init_workspace
|
|
42
42
|
|
|
43
43
|
__all__ = [
|
|
@@ -65,6 +65,7 @@ __all__ = [
|
|
|
65
65
|
"external_program",
|
|
66
66
|
"is_numeric",
|
|
67
67
|
"init_workspace",
|
|
68
|
+
"infer_lookup_rows",
|
|
68
69
|
"iter_sequential",
|
|
69
70
|
"layout_to_dict",
|
|
70
71
|
"parse_copybook",
|
|
@@ -89,4 +90,4 @@ __all__ = [
|
|
|
89
90
|
"write_s3_parquet",
|
|
90
91
|
]
|
|
91
92
|
|
|
92
|
-
__version__ = "0.
|
|
93
|
+
__version__ = "0.3.1"
|
{mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/cli.py
RENAMED
|
@@ -20,7 +20,7 @@ from .similarity import (
|
|
|
20
20
|
write_results,
|
|
21
21
|
)
|
|
22
22
|
from .specs import inspect_project, write_inspection
|
|
23
|
-
from .synthetic import synthetic_rows
|
|
23
|
+
from .synthetic import infer_lookup_rows, synthetic_rows
|
|
24
24
|
from .workspace import init_workspace
|
|
25
25
|
|
|
26
26
|
|
|
@@ -84,6 +84,10 @@ def _similarity_scan(args: argparse.Namespace) -> int:
|
|
|
84
84
|
threshold=args.threshold,
|
|
85
85
|
shingle_size=args.shingle_size,
|
|
86
86
|
exhaustive_limit=args.exhaustive_limit,
|
|
87
|
+
algorithm=args.algorithm,
|
|
88
|
+
partial_threshold=args.partial_threshold,
|
|
89
|
+
min_clone_tokens=args.min_clone_tokens,
|
|
90
|
+
cluster_policy=args.cluster_policy,
|
|
87
91
|
)
|
|
88
92
|
write_results(report, args.json, args.csv)
|
|
89
93
|
write_ai_review_payload(report, args.review_request)
|
|
@@ -148,6 +152,65 @@ def _synthetic(args: argparse.Namespace) -> int:
|
|
|
148
152
|
return 0
|
|
149
153
|
|
|
150
154
|
|
|
155
|
+
def _synthetic_lookup(args: argparse.Namespace) -> int:
|
|
156
|
+
source = Path(args.source).resolve()
|
|
157
|
+
output = Path(args.output).resolve()
|
|
158
|
+
if output == source or (source.is_dir() and output.is_relative_to(source)):
|
|
159
|
+
raise ValueError("lookup output must be outside the golden source")
|
|
160
|
+
source_format = args.source_format
|
|
161
|
+
if source_format == "auto":
|
|
162
|
+
source_format = "parquet" if source.is_dir() or source.suffix.casefold() == ".parquet" else "csv"
|
|
163
|
+
columns = [*args.key, args.value]
|
|
164
|
+
if source_format == "csv":
|
|
165
|
+
from .io import read_delimited_rows
|
|
166
|
+
|
|
167
|
+
source_rows = list(read_delimited_rows(source, delimiter=args.delimiter))
|
|
168
|
+
else:
|
|
169
|
+
try:
|
|
170
|
+
import pyarrow.dataset as arrow_dataset
|
|
171
|
+
except ImportError as exc:
|
|
172
|
+
raise RuntimeError("install mainframe-migration-toolkit[parquet]") from exc
|
|
173
|
+
source_rows = arrow_dataset.dataset(str(source), format="parquet").to_table(columns=columns).to_pylist()
|
|
174
|
+
|
|
175
|
+
lookup = infer_lookup_rows(source_rows, keys=args.key, value=args.value)
|
|
176
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
output_format = args.output_format
|
|
178
|
+
if output_format == "auto":
|
|
179
|
+
output_format = "parquet" if output.suffix.casefold() == ".parquet" else "csv"
|
|
180
|
+
if output_format == "csv":
|
|
181
|
+
with output.open("w", encoding="utf-8", newline="") as stream:
|
|
182
|
+
writer = csv.DictWriter(stream, fieldnames=columns, delimiter=args.delimiter)
|
|
183
|
+
writer.writeheader()
|
|
184
|
+
writer.writerows(lookup)
|
|
185
|
+
else:
|
|
186
|
+
try:
|
|
187
|
+
import pyarrow as arrow
|
|
188
|
+
import pyarrow.parquet as parquet
|
|
189
|
+
except ImportError as exc:
|
|
190
|
+
raise RuntimeError("install mainframe-migration-toolkit[parquet]") from exc
|
|
191
|
+
table = arrow.Table.from_pylist(lookup) if lookup else arrow.table({name: [] for name in columns})
|
|
192
|
+
parquet.write_table(table, output)
|
|
193
|
+
|
|
194
|
+
manifest = Path(args.manifest) if args.manifest else output.with_suffix(output.suffix + ".manifest.json")
|
|
195
|
+
_json_dump(
|
|
196
|
+
{
|
|
197
|
+
"synthetic": True,
|
|
198
|
+
"kind": "inferred-external-lookup",
|
|
199
|
+
"program": args.program,
|
|
200
|
+
"arguments": args.key,
|
|
201
|
+
"result": args.value,
|
|
202
|
+
"inferred_from": str(source),
|
|
203
|
+
"output": str(output),
|
|
204
|
+
"format": output_format,
|
|
205
|
+
"rows": len(lookup),
|
|
206
|
+
"sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
|
|
207
|
+
},
|
|
208
|
+
manifest,
|
|
209
|
+
)
|
|
210
|
+
_json_dump({"output": str(output), "manifest": str(manifest), "rows": len(lookup)})
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
|
|
151
214
|
def _toolkit_root() -> Path:
|
|
152
215
|
override = os.environ.get("MAINFRAME_TOOLKIT_ROOT")
|
|
153
216
|
if override:
|
|
@@ -293,6 +356,10 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
293
356
|
scan.add_argument("--threshold", type=float, default=80.0)
|
|
294
357
|
scan.add_argument("--shingle-size", type=int, default=3)
|
|
295
358
|
scan.add_argument("--exhaustive-limit", type=int, default=80)
|
|
359
|
+
scan.add_argument("--algorithm", choices=("advanced", "classic"), default="advanced")
|
|
360
|
+
scan.add_argument("--partial-threshold", type=float, default=75.0)
|
|
361
|
+
scan.add_argument("--min-clone-tokens", type=int, default=30)
|
|
362
|
+
scan.add_argument("--cluster-policy", choices=("cohesive", "connected"), default="connected")
|
|
296
363
|
scan.add_argument("--json", default="similarity.json")
|
|
297
364
|
scan.add_argument("--csv", default="similarity.csv")
|
|
298
365
|
scan.add_argument("--review-request", default="similarity-review-request.json")
|
|
@@ -314,6 +381,21 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
314
381
|
synthetic.add_argument("--delimiter", default=",")
|
|
315
382
|
synthetic.set_defaults(handler=_synthetic)
|
|
316
383
|
|
|
384
|
+
synthetic_lookup = subcommands.add_parser(
|
|
385
|
+
"synthetic-lookup",
|
|
386
|
+
help="infer a missing external lookup from golden columns",
|
|
387
|
+
)
|
|
388
|
+
synthetic_lookup.add_argument("source")
|
|
389
|
+
synthetic_lookup.add_argument("output")
|
|
390
|
+
synthetic_lookup.add_argument("--key", action="append", required=True)
|
|
391
|
+
synthetic_lookup.add_argument("--value", required=True)
|
|
392
|
+
synthetic_lookup.add_argument("--program", required=True)
|
|
393
|
+
synthetic_lookup.add_argument("--source-format", choices=("auto", "csv", "parquet"), default="auto")
|
|
394
|
+
synthetic_lookup.add_argument("--output-format", choices=("auto", "csv", "parquet"), default="auto")
|
|
395
|
+
synthetic_lookup.add_argument("--delimiter", default=",")
|
|
396
|
+
synthetic_lookup.add_argument("--manifest")
|
|
397
|
+
synthetic_lookup.set_defaults(handler=_synthetic_lookup)
|
|
398
|
+
|
|
317
399
|
golden = subcommands.add_parser("golden", help="Java cell-level dataset validator").add_subparsers(dest="golden_command", required=True)
|
|
318
400
|
build = golden.add_parser("build")
|
|
319
401
|
build.add_argument("--validator-dir")
|
|
@@ -81,6 +81,45 @@ _LSH_BANDS = 12
|
|
|
81
81
|
_MAX_LSH_BUCKET = 128
|
|
82
82
|
_MAX_REPORTED_PAIRS = 5_000
|
|
83
83
|
|
|
84
|
+
_COBOL_STRUCTURAL_WORDS = frozenset(
|
|
85
|
+
{
|
|
86
|
+
"ACCEPT", "ADD", "CALL", "CANCEL", "CLOSE", "COMPUTE", "CONTINUE",
|
|
87
|
+
"DELETE", "DISPLAY", "DIVIDE", "ELSE", "END-CALL", "END-COMPUTE",
|
|
88
|
+
"END-EVALUATE", "END-IF", "END-PERFORM", "END-READ", "END-SEARCH",
|
|
89
|
+
"END-STRING", "END-WRITE", "ENTRY", "EVALUATE", "EXIT", "GOBACK",
|
|
90
|
+
"GO", "IF", "INITIALIZE", "INSPECT", "MERGE", "MOVE", "MULTIPLY",
|
|
91
|
+
"OPEN", "PERFORM", "READ", "RELEASE", "RETURN", "REWRITE", "SEARCH",
|
|
92
|
+
"SET", "SORT", "START", "STOP", "STRING", "SUBTRACT", "UNSTRING",
|
|
93
|
+
"WHEN", "WRITE", "IDENTIFICATION", "ENVIRONMENT", "DATA", "PROCEDURE",
|
|
94
|
+
"DIVISION", "SECTION", "DECLARATIVES", "END-DECLARATIVES",
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
_COBOL_LANGUAGE_WORDS = _COBOL_STRUCTURAL_WORDS | frozenset(
|
|
98
|
+
{
|
|
99
|
+
"PROGRAM-ID", "FILE-CONTROL", "SELECT", "ASSIGN", "FD", "SD", "PIC",
|
|
100
|
+
"PICTURE", "VALUE", "VALUES", "OCCURS", "REDEFINES", "RENAMES", "USAGE",
|
|
101
|
+
"COMP", "COMP-1", "COMP-2", "COMP-3", "BINARY", "PACKED-DECIMAL",
|
|
102
|
+
"DISPLAY-1", "WORKING-STORAGE", "LINKAGE", "FILE", "INPUT-OUTPUT",
|
|
103
|
+
"USING", "GIVING", "THRU", "THROUGH", "UNTIL", "VARYING", "TIMES",
|
|
104
|
+
"FROM", "BY", "TO", "INTO", "OF", "IN", "ON", "AT", "END", "NOT",
|
|
105
|
+
"AND", "OR", "TRUE", "FALSE", "ZERO", "ZEROS", "ZEROES", "SPACE",
|
|
106
|
+
"SPACES", "HIGH-VALUES", "LOW-VALUES", "ALL", "CORRESPONDING", "ROUNDED",
|
|
107
|
+
"SIZE", "ERROR", "INVALID", "KEY", "WITH", "TEST", "AFTER", "BEFORE",
|
|
108
|
+
"ASCENDING", "DESCENDING", "DUPLICATES", "NO", "ADVANCING", "UPON",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
_JCL_LANGUAGE_WORDS = _JCL_OPERATIONS | frozenset(
|
|
112
|
+
{
|
|
113
|
+
"PGM", "PROC", "PARM", "COND", "DSN", "DSNAME", "DISP", "UNIT",
|
|
114
|
+
"SPACE", "DCB", "SYSOUT", "CLASS", "MSGCLASS", "REGION", "TIME",
|
|
115
|
+
"RESTART", "SYMBOLS", "MEMBER", "ORDER", "INCLUDE", "SET", "IF",
|
|
116
|
+
"THEN", "ELSE", "ENDIF", "PASS", "KEEP", "CATLG", "DELETE", "SHR",
|
|
117
|
+
"OLD", "NEW", "MOD", "DUMMY", "DATA", "DLM",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
_IDENTIFIER_RE = re.compile(r"[A-Z_$#@&][A-Z0-9_$#@&.-]*\Z")
|
|
121
|
+
_NUMBER_RE = re.compile(r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)\Z")
|
|
122
|
+
|
|
84
123
|
|
|
85
124
|
@dataclass(frozen=True, slots=True)
|
|
86
125
|
class SourceDocument:
|
|
@@ -91,6 +130,9 @@ class SourceDocument:
|
|
|
91
130
|
content: str = field(repr=False)
|
|
92
131
|
normalized: str = field(repr=False)
|
|
93
132
|
tokens: tuple[str, ...] = field(repr=False)
|
|
133
|
+
abstract_tokens: tuple[str, ...] = field(repr=False)
|
|
134
|
+
structure_tokens: tuple[str, ...] = field(repr=False)
|
|
135
|
+
literal_tokens: tuple[str, ...] = field(repr=False)
|
|
94
136
|
digest: str
|
|
95
137
|
source_id: str
|
|
96
138
|
|
|
@@ -100,22 +142,30 @@ class SimilarityReport:
|
|
|
100
142
|
threshold: float
|
|
101
143
|
shingle_size: int
|
|
102
144
|
exhaustive_limit: int
|
|
145
|
+
algorithm: str
|
|
146
|
+
partial_threshold: float
|
|
147
|
+
cluster_policy: str
|
|
103
148
|
records: list[dict[str, Any]]
|
|
104
149
|
clusters: list[dict[str, Any]]
|
|
105
150
|
statistics: dict[str, Any]
|
|
151
|
+
partial_clones: list[dict[str, Any]] = field(default_factory=list)
|
|
106
152
|
documents: tuple[SourceDocument, ...] = field(default_factory=tuple, repr=False)
|
|
107
153
|
|
|
108
154
|
def to_dict(self) -> dict[str, Any]:
|
|
109
155
|
return {
|
|
110
|
-
"schema_version":
|
|
156
|
+
"schema_version": 2,
|
|
111
157
|
"configuracao": {
|
|
112
158
|
"limiar_percentual": self.threshold,
|
|
113
159
|
"tamanho_shingle": self.shingle_size,
|
|
114
160
|
"limite_comparacao_exaustiva": self.exhaustive_limit,
|
|
161
|
+
"algoritmo": self.algorithm,
|
|
162
|
+
"limiar_clone_parcial": self.partial_threshold,
|
|
163
|
+
"politica_cluster": self.cluster_policy,
|
|
115
164
|
},
|
|
116
165
|
"resumo": deepcopy(self.statistics),
|
|
117
166
|
"arquivos": deepcopy(self.records),
|
|
118
167
|
"clusters": deepcopy(self.clusters),
|
|
168
|
+
"clones_parciais": deepcopy(self.partial_clones),
|
|
119
169
|
}
|
|
120
170
|
|
|
121
171
|
def __getitem__(self, key: str) -> Any:
|
|
@@ -409,6 +459,38 @@ def tokenize_normalized(text: str) -> tuple[str, ...]:
|
|
|
409
459
|
return tuple(_TOKEN_RE.findall(text))
|
|
410
460
|
|
|
411
461
|
|
|
462
|
+
def _is_literal(token: str) -> bool:
|
|
463
|
+
return (
|
|
464
|
+
len(token) >= 2
|
|
465
|
+
and token[0] == token[-1]
|
|
466
|
+
and token[0] in {"'", '"'}
|
|
467
|
+
) or _NUMBER_RE.fullmatch(token) is not None
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _semantic_features(tokens: Sequence[str], tipo: str) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
|
|
471
|
+
language_words = _COBOL_LANGUAGE_WORDS if tipo == "cobol" else _JCL_LANGUAGE_WORDS
|
|
472
|
+
structural_words = _COBOL_STRUCTURAL_WORDS if tipo == "cobol" else _JCL_LANGUAGE_WORDS
|
|
473
|
+
identifiers: dict[str, str] = {}
|
|
474
|
+
abstract: list[str] = []
|
|
475
|
+
structure: list[str] = []
|
|
476
|
+
literals: list[str] = []
|
|
477
|
+
|
|
478
|
+
for token in tokens:
|
|
479
|
+
upper = token.upper()
|
|
480
|
+
if _is_literal(token):
|
|
481
|
+
literals.append(token)
|
|
482
|
+
abstract.append(token)
|
|
483
|
+
elif upper in language_words or not _IDENTIFIER_RE.fullmatch(upper):
|
|
484
|
+
abstract.append(upper)
|
|
485
|
+
else:
|
|
486
|
+
placeholder = identifiers.setdefault(upper, f"__ID{len(identifiers) + 1}__")
|
|
487
|
+
abstract.append(placeholder)
|
|
488
|
+
if upper in structural_words:
|
|
489
|
+
structure.append(upper)
|
|
490
|
+
|
|
491
|
+
return tuple(abstract), tuple(structure), tuple(literals)
|
|
492
|
+
|
|
493
|
+
|
|
412
494
|
def _stable_digest(value: str, *, size: int = 16) -> str:
|
|
413
495
|
return hashlib.blake2b(value.encode("utf-8"), digest_size=size).hexdigest()
|
|
414
496
|
|
|
@@ -423,6 +505,8 @@ def _make_document(
|
|
|
423
505
|
kind = _canonical_kind(tipo)
|
|
424
506
|
relative = Path(relative_path).as_posix()
|
|
425
507
|
normalized = normalize_source(content, kind)
|
|
508
|
+
tokens = tokenize_normalized(normalized)
|
|
509
|
+
abstract_tokens, structure_tokens, literal_tokens = _semantic_features(tokens, kind)
|
|
426
510
|
return SourceDocument(
|
|
427
511
|
tipo=kind,
|
|
428
512
|
nome=name,
|
|
@@ -430,7 +514,10 @@ def _make_document(
|
|
|
430
514
|
relative_path=relative,
|
|
431
515
|
content=content,
|
|
432
516
|
normalized=normalized,
|
|
433
|
-
tokens=
|
|
517
|
+
tokens=tokens,
|
|
518
|
+
abstract_tokens=abstract_tokens,
|
|
519
|
+
structure_tokens=structure_tokens,
|
|
520
|
+
literal_tokens=literal_tokens,
|
|
434
521
|
digest=_stable_digest(normalized),
|
|
435
522
|
source_id=_stable_digest(f"{kind}\0{relative}", size=10),
|
|
436
523
|
)
|
|
@@ -538,6 +625,34 @@ def _dice(left: Counter[Any], right: Counter[Any]) -> float:
|
|
|
538
625
|
return (2.0 * overlap) / total
|
|
539
626
|
|
|
540
627
|
|
|
628
|
+
def _containment(left: Counter[Any], right: Counter[Any]) -> float:
|
|
629
|
+
smallest = min(sum(left.values()), sum(right.values()))
|
|
630
|
+
if smallest == 0:
|
|
631
|
+
return 1.0 if not left and not right else 0.0
|
|
632
|
+
return sum((left & right).values()) / smallest
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@dataclass(frozen=True, slots=True)
|
|
636
|
+
class PairScore:
|
|
637
|
+
percentage: float
|
|
638
|
+
content: float
|
|
639
|
+
order: float
|
|
640
|
+
renamed: float
|
|
641
|
+
structure: float
|
|
642
|
+
literals: float
|
|
643
|
+
partial: float
|
|
644
|
+
|
|
645
|
+
def to_dict(self) -> dict[str, float]:
|
|
646
|
+
return {
|
|
647
|
+
"conteudo": self.content,
|
|
648
|
+
"ordem": self.order,
|
|
649
|
+
"renomeacao": self.renamed,
|
|
650
|
+
"estrutura": self.structure,
|
|
651
|
+
"literais": self.literals,
|
|
652
|
+
"clone_parcial": self.partial,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
|
|
541
656
|
def _similarity_tokens(
|
|
542
657
|
left: Sequence[str], right: Sequence[str], shingle_size: int
|
|
543
658
|
) -> float:
|
|
@@ -553,21 +668,73 @@ def _similarity_tokens(
|
|
|
553
668
|
return round(100.0 * ((0.25 * ordered) + (0.75 * lexical)), 2)
|
|
554
669
|
|
|
555
670
|
|
|
671
|
+
def _advanced_similarity(left: SourceDocument, right: SourceDocument, shingle_size: int) -> PairScore:
|
|
672
|
+
if left.tokens == right.tokens:
|
|
673
|
+
return PairScore(100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0)
|
|
674
|
+
|
|
675
|
+
width = max(1, min(shingle_size, len(left.tokens), len(right.tokens)))
|
|
676
|
+
left_words = _feature_counter(left.tokens, 1)
|
|
677
|
+
right_words = _feature_counter(right.tokens, 1)
|
|
678
|
+
left_ordered = _feature_counter(left.tokens, width)
|
|
679
|
+
right_ordered = _feature_counter(right.tokens, width)
|
|
680
|
+
content = 100.0 * _dice(left_words, right_words)
|
|
681
|
+
order = 100.0 * _dice(left_ordered, right_ordered)
|
|
682
|
+
renamed = 100.0 * _dice(
|
|
683
|
+
_feature_counter(left.abstract_tokens, width),
|
|
684
|
+
_feature_counter(right.abstract_tokens, width),
|
|
685
|
+
)
|
|
686
|
+
structure = 100.0 * _dice(
|
|
687
|
+
_feature_counter(left.structure_tokens, 2),
|
|
688
|
+
_feature_counter(right.structure_tokens, 2),
|
|
689
|
+
)
|
|
690
|
+
literals = 100.0 * _dice(
|
|
691
|
+
_feature_counter(left.literal_tokens, 1),
|
|
692
|
+
_feature_counter(right.literal_tokens, 1),
|
|
693
|
+
)
|
|
694
|
+
partial = 100.0 * max(
|
|
695
|
+
_containment(left_ordered, right_ordered),
|
|
696
|
+
_containment(
|
|
697
|
+
_feature_counter(left.abstract_tokens, width),
|
|
698
|
+
_feature_counter(right.abstract_tokens, width),
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
percentage = (
|
|
702
|
+
(0.15 * content)
|
|
703
|
+
+ (0.15 * order)
|
|
704
|
+
+ (0.35 * renamed)
|
|
705
|
+
+ (0.20 * structure)
|
|
706
|
+
+ (0.15 * literals)
|
|
707
|
+
)
|
|
708
|
+
return PairScore(
|
|
709
|
+
*(
|
|
710
|
+
round(value, 2)
|
|
711
|
+
for value in (percentage, content, order, renamed, structure, literals, partial)
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _classic_pair_score(left: SourceDocument, right: SourceDocument, shingle_size: int) -> PairScore:
|
|
717
|
+
percentage = _similarity_tokens(left.tokens, right.tokens, shingle_size)
|
|
718
|
+
return PairScore(percentage, percentage, percentage, percentage, percentage, percentage, percentage)
|
|
719
|
+
|
|
720
|
+
|
|
556
721
|
def similarity_percent(
|
|
557
722
|
left: str,
|
|
558
723
|
right: str,
|
|
559
724
|
*,
|
|
560
725
|
tipo: str | None = None,
|
|
561
726
|
shingle_size: int = 3,
|
|
727
|
+
algorithm: str = "advanced",
|
|
562
728
|
) -> float:
|
|
563
729
|
if shingle_size < 1:
|
|
564
730
|
raise ValueError("shingle_size deve ser positivo")
|
|
565
|
-
if
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
731
|
+
if algorithm not in {"advanced", "classic"}:
|
|
732
|
+
raise ValueError("algorithm deve ser advanced ou classic")
|
|
733
|
+
kind = _canonical_kind(tipo or "cobol")
|
|
734
|
+
left_document = _make_document(kind, "LEFT", "LEFT", left)
|
|
735
|
+
right_document = _make_document(kind, "RIGHT", "RIGHT", right)
|
|
736
|
+
scorer = _advanced_similarity if algorithm == "advanced" else _classic_pair_score
|
|
737
|
+
return scorer(left_document, right_document, shingle_size).percentage
|
|
571
738
|
|
|
572
739
|
|
|
573
740
|
def _shingle_hashes(tokens: Sequence[str], width: int) -> set[int]:
|
|
@@ -620,6 +787,7 @@ def _candidate_pairs(
|
|
|
620
787
|
*,
|
|
621
788
|
shingle_size: int,
|
|
622
789
|
exhaustive_limit: int,
|
|
790
|
+
algorithm: str,
|
|
623
791
|
) -> tuple[set[tuple[int, int]], str]:
|
|
624
792
|
candidates: set[tuple[int, int]] = set()
|
|
625
793
|
indices_by_kind: dict[str, list[int]] = defaultdict(list)
|
|
@@ -641,17 +809,21 @@ def _candidate_pairs(
|
|
|
641
809
|
anchor = min(bucket)
|
|
642
810
|
candidates.update((anchor, item) for item in sorted(bucket) if item != anchor)
|
|
643
811
|
|
|
644
|
-
band_buckets: dict[tuple[int, tuple[int, ...]], list[int]] = defaultdict(list)
|
|
812
|
+
band_buckets: dict[tuple[str, int, tuple[int, ...]], list[int]] = defaultdict(list)
|
|
645
813
|
rows = _MINHASH_SIZE // _LSH_BANDS
|
|
646
814
|
# One representative per exact digest keeps duplicate-heavy scans linear.
|
|
647
815
|
representatives = sorted(min(bucket) for bucket in exact_groups.values())
|
|
648
816
|
for index in representatives:
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
for
|
|
653
|
-
|
|
654
|
-
|
|
817
|
+
feature_streams = [("raw", documents[index].tokens)]
|
|
818
|
+
if algorithm == "advanced":
|
|
819
|
+
feature_streams.append(("abstract", documents[index].abstract_tokens))
|
|
820
|
+
for feature_name, feature_tokens in feature_streams:
|
|
821
|
+
signature = _minhash_signature(
|
|
822
|
+
_shingle_hashes(feature_tokens, shingle_size)
|
|
823
|
+
)
|
|
824
|
+
for band in range(_LSH_BANDS):
|
|
825
|
+
start = band * rows
|
|
826
|
+
band_buckets[(feature_name, band, signature[start : start + rows])].append(index)
|
|
655
827
|
for bucket in band_buckets.values():
|
|
656
828
|
if len(bucket) > 1:
|
|
657
829
|
_add_bucket_pairs(bucket, candidates)
|
|
@@ -709,6 +881,10 @@ def analyze_documents(
|
|
|
709
881
|
threshold: float = 80.0,
|
|
710
882
|
shingle_size: int = 3,
|
|
711
883
|
exhaustive_limit: int = 80,
|
|
884
|
+
algorithm: str = "advanced",
|
|
885
|
+
partial_threshold: float = 75.0,
|
|
886
|
+
min_clone_tokens: int = 30,
|
|
887
|
+
cluster_policy: str = "connected",
|
|
712
888
|
) -> SimilarityReport:
|
|
713
889
|
if not 0 <= threshold <= 100:
|
|
714
890
|
raise ValueError("threshold deve estar entre 0 e 100")
|
|
@@ -716,6 +892,14 @@ def analyze_documents(
|
|
|
716
892
|
raise ValueError("shingle_size deve ser positivo")
|
|
717
893
|
if exhaustive_limit < 2:
|
|
718
894
|
raise ValueError("exhaustive_limit deve ser pelo menos 2")
|
|
895
|
+
if algorithm not in {"advanced", "classic"}:
|
|
896
|
+
raise ValueError("algorithm deve ser advanced ou classic")
|
|
897
|
+
if not 0 <= partial_threshold <= 100:
|
|
898
|
+
raise ValueError("partial_threshold deve estar entre 0 e 100")
|
|
899
|
+
if min_clone_tokens < 1:
|
|
900
|
+
raise ValueError("min_clone_tokens deve ser positivo")
|
|
901
|
+
if cluster_policy not in {"cohesive", "connected"}:
|
|
902
|
+
raise ValueError("cluster_policy deve ser cohesive ou connected")
|
|
719
903
|
|
|
720
904
|
ordered = sorted(
|
|
721
905
|
documents,
|
|
@@ -733,24 +917,55 @@ def analyze_documents(
|
|
|
733
917
|
ordered,
|
|
734
918
|
shingle_size=shingle_size,
|
|
735
919
|
exhaustive_limit=exhaustive_limit,
|
|
920
|
+
algorithm=algorithm,
|
|
736
921
|
)
|
|
737
|
-
score_cache: dict[tuple[int, int],
|
|
922
|
+
score_cache: dict[tuple[int, int], PairScore] = {}
|
|
738
923
|
|
|
739
|
-
def score(left: int, right: int) ->
|
|
924
|
+
def score(left: int, right: int) -> PairScore:
|
|
740
925
|
key = _pair_key(left, right)
|
|
741
926
|
if key not in score_cache:
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
)
|
|
927
|
+
scorer = _advanced_similarity if algorithm == "advanced" else _classic_pair_score
|
|
928
|
+
score_cache[key] = scorer(ordered[key[0]], ordered[key[1]], shingle_size)
|
|
745
929
|
return score_cache[key]
|
|
746
930
|
|
|
747
931
|
union_find = _UnionFind(len(ordered))
|
|
748
|
-
|
|
932
|
+
component_members: dict[int, set[int]] = {index: {index} for index in range(len(ordered))}
|
|
933
|
+
qualifying_edges: list[tuple[float, int, int]] = []
|
|
749
934
|
for left, right in sorted(candidates):
|
|
750
|
-
percentage = score(left, right)
|
|
935
|
+
percentage = score(left, right).percentage
|
|
751
936
|
if percentage >= threshold:
|
|
752
|
-
|
|
753
|
-
|
|
937
|
+
qualifying_edges.append((percentage, left, right))
|
|
938
|
+
|
|
939
|
+
rejected_by_cohesion = 0
|
|
940
|
+
cohesion_limit_rejections = 0
|
|
941
|
+
for _, left, right in sorted(qualifying_edges, key=lambda item: (-item[0], item[1], item[2])):
|
|
942
|
+
left_root = union_find.find(left)
|
|
943
|
+
right_root = union_find.find(right)
|
|
944
|
+
if left_root == right_root:
|
|
945
|
+
continue
|
|
946
|
+
left_members = component_members[left_root]
|
|
947
|
+
right_members = component_members[right_root]
|
|
948
|
+
should_merge = True
|
|
949
|
+
if cluster_policy == "cohesive":
|
|
950
|
+
combined = left_members | right_members
|
|
951
|
+
identical = len({ordered[index].digest for index in combined}) == 1
|
|
952
|
+
if len(combined) > exhaustive_limit and not identical:
|
|
953
|
+
should_merge = False
|
|
954
|
+
cohesion_limit_rejections += 1
|
|
955
|
+
elif not identical:
|
|
956
|
+
should_merge = all(
|
|
957
|
+
score(member_left, member_right).percentage >= threshold
|
|
958
|
+
for member_left in sorted(left_members)
|
|
959
|
+
for member_right in sorted(right_members)
|
|
960
|
+
)
|
|
961
|
+
if not should_merge:
|
|
962
|
+
rejected_by_cohesion += 1
|
|
963
|
+
continue
|
|
964
|
+
union_find.union(left_root, right_root)
|
|
965
|
+
merged_root = union_find.find(left_root)
|
|
966
|
+
old_root = right_root if merged_root == left_root else left_root
|
|
967
|
+
component_members[merged_root] = left_members | right_members
|
|
968
|
+
component_members.pop(old_root, None)
|
|
754
969
|
|
|
755
970
|
components: dict[int, list[int]] = defaultdict(list)
|
|
756
971
|
for index in range(len(ordered)):
|
|
@@ -794,13 +1009,15 @@ def analyze_documents(
|
|
|
794
1009
|
if pair_index >= _MAX_REPORTED_PAIRS:
|
|
795
1010
|
exact_interval = False
|
|
796
1011
|
break
|
|
797
|
-
|
|
1012
|
+
pair_score = score(left, right)
|
|
1013
|
+
percentage = pair_score.percentage
|
|
798
1014
|
pair_scores.append(percentage)
|
|
799
1015
|
pair_details.append(
|
|
800
1016
|
{
|
|
801
1017
|
"origem": ordered[left].nome,
|
|
802
1018
|
"destino": ordered[right].nome,
|
|
803
1019
|
"percentual": percentage,
|
|
1020
|
+
"componentes": pair_score.to_dict(),
|
|
804
1021
|
}
|
|
805
1022
|
)
|
|
806
1023
|
percentage_range = _format_range(pair_scores)
|
|
@@ -846,6 +1063,28 @@ def analyze_documents(
|
|
|
846
1063
|
count * (count - 1) // 2
|
|
847
1064
|
for count in Counter(document.tipo for document in ordered).values()
|
|
848
1065
|
)
|
|
1066
|
+
partial_clones: list[dict[str, Any]] = []
|
|
1067
|
+
if algorithm == "advanced":
|
|
1068
|
+
for (left, right), pair_score in sorted(score_cache.items()):
|
|
1069
|
+
minimum_tokens = min(len(ordered[left].tokens), len(ordered[right].tokens))
|
|
1070
|
+
if (
|
|
1071
|
+
pair_score.percentage < threshold
|
|
1072
|
+
and pair_score.partial >= partial_threshold
|
|
1073
|
+
and minimum_tokens >= min_clone_tokens
|
|
1074
|
+
):
|
|
1075
|
+
partial_clones.append(
|
|
1076
|
+
{
|
|
1077
|
+
"tipo": ordered[left].tipo,
|
|
1078
|
+
"origem": ordered[left].nome,
|
|
1079
|
+
"destino": ordered[right].nome,
|
|
1080
|
+
"percentual_global": pair_score.percentage,
|
|
1081
|
+
"percentual_contencao": pair_score.partial,
|
|
1082
|
+
"componentes": pair_score.to_dict(),
|
|
1083
|
+
}
|
|
1084
|
+
)
|
|
1085
|
+
partial_clones.sort(
|
|
1086
|
+
key=lambda item: (-item["percentual_contencao"], item["tipo"], item["origem"], item["destino"])
|
|
1087
|
+
)
|
|
849
1088
|
statistics = {
|
|
850
1089
|
"arquivos": len(ordered),
|
|
851
1090
|
"clusters": len(clusters),
|
|
@@ -853,16 +1092,25 @@ def analyze_documents(
|
|
|
853
1092
|
"pares_possiveis": possible_pairs,
|
|
854
1093
|
"pares_candidatos": len(candidates),
|
|
855
1094
|
"pares_comparados": len(score_cache),
|
|
856
|
-
"pares_acima_limiar":
|
|
1095
|
+
"pares_acima_limiar": len(qualifying_edges),
|
|
857
1096
|
"modo_candidatos": mode,
|
|
1097
|
+
"algoritmo": algorithm,
|
|
1098
|
+
"clones_parciais": len(partial_clones),
|
|
1099
|
+
"politica_cluster": cluster_policy,
|
|
1100
|
+
"unioes_rejeitadas_por_coesao": rejected_by_cohesion,
|
|
1101
|
+
"unioes_rejeitadas_por_limite": cohesion_limit_rejections,
|
|
858
1102
|
}
|
|
859
1103
|
return SimilarityReport(
|
|
860
1104
|
threshold=float(threshold),
|
|
861
1105
|
shingle_size=shingle_size,
|
|
862
1106
|
exhaustive_limit=exhaustive_limit,
|
|
1107
|
+
algorithm=algorithm,
|
|
1108
|
+
partial_threshold=float(partial_threshold),
|
|
1109
|
+
cluster_policy=cluster_policy,
|
|
863
1110
|
records=records,
|
|
864
1111
|
clusters=clusters,
|
|
865
1112
|
statistics=statistics,
|
|
1113
|
+
partial_clones=partial_clones,
|
|
866
1114
|
documents=tuple(ordered),
|
|
867
1115
|
)
|
|
868
1116
|
|
|
@@ -880,6 +1128,10 @@ def analyze_codebase(
|
|
|
880
1128
|
threshold: float = 80.0,
|
|
881
1129
|
shingle_size: int = 3,
|
|
882
1130
|
exhaustive_limit: int = 80,
|
|
1131
|
+
algorithm: str = "advanced",
|
|
1132
|
+
partial_threshold: float = 75.0,
|
|
1133
|
+
min_clone_tokens: int = 30,
|
|
1134
|
+
cluster_policy: str = "connected",
|
|
883
1135
|
ignored_directories: Iterable[str] = _IGNORED_DIRECTORIES,
|
|
884
1136
|
) -> SimilarityReport:
|
|
885
1137
|
return analyze_documents(
|
|
@@ -887,6 +1139,10 @@ def analyze_codebase(
|
|
|
887
1139
|
threshold=threshold,
|
|
888
1140
|
shingle_size=shingle_size,
|
|
889
1141
|
exhaustive_limit=exhaustive_limit,
|
|
1142
|
+
algorithm=algorithm,
|
|
1143
|
+
partial_threshold=partial_threshold,
|
|
1144
|
+
min_clone_tokens=min_clone_tokens,
|
|
1145
|
+
cluster_policy=cluster_policy,
|
|
890
1146
|
)
|
|
891
1147
|
|
|
892
1148
|
|
|
@@ -972,7 +1228,7 @@ def build_ai_review_payload(
|
|
|
972
1228
|
)
|
|
973
1229
|
|
|
974
1230
|
return {
|
|
975
|
-
"schema_version":
|
|
1231
|
+
"schema_version": 2,
|
|
976
1232
|
"tarefa": (
|
|
977
1233
|
"Revise cada cluster pela semantica do codigo e retorne um parecer "
|
|
978
1234
|
"similar, identico ou diferente, com justificativa breve."
|
|
@@ -988,6 +1244,7 @@ def build_ai_review_payload(
|
|
|
988
1244
|
]
|
|
989
1245
|
},
|
|
990
1246
|
"clusters": clusters,
|
|
1247
|
+
"clones_parciais": deepcopy(payload.get("clones_parciais", [])),
|
|
991
1248
|
}
|
|
992
1249
|
|
|
993
1250
|
|
|
@@ -4,7 +4,7 @@ import hashlib
|
|
|
4
4
|
import random
|
|
5
5
|
from datetime import date, datetime, timedelta
|
|
6
6
|
from decimal import Decimal
|
|
7
|
-
from typing import Any, Mapping
|
|
7
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
8
8
|
|
|
9
9
|
from .errors import OptionalDependencyError
|
|
10
10
|
|
|
@@ -54,6 +54,44 @@ def synthetic_dataframe(
|
|
|
54
54
|
return spark.createDataFrame(rows, schema=spark_schema)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def infer_lookup_rows(
|
|
58
|
+
rows: Iterable[Mapping[str, Any]],
|
|
59
|
+
*,
|
|
60
|
+
keys: Sequence[str],
|
|
61
|
+
value: str,
|
|
62
|
+
) -> list[dict[str, Any]]:
|
|
63
|
+
"""Reconstruct a missing deterministic lookup from observable golden columns."""
|
|
64
|
+
key_names = tuple(keys)
|
|
65
|
+
if not key_names:
|
|
66
|
+
raise ValueError("at least one lookup key is required")
|
|
67
|
+
if len(set(key_names)) != len(key_names) or value in key_names:
|
|
68
|
+
raise ValueError("lookup keys and value must be distinct")
|
|
69
|
+
|
|
70
|
+
inferred: dict[tuple[Any, ...], Any] = {}
|
|
71
|
+
for row_number, row in enumerate(rows, start=1):
|
|
72
|
+
missing = [name for name in (*key_names, value) if name not in row]
|
|
73
|
+
if missing:
|
|
74
|
+
raise ValueError(f"missing columns at row {row_number}: {', '.join(missing)}")
|
|
75
|
+
key = tuple(row[name] for name in key_names)
|
|
76
|
+
result = row[value]
|
|
77
|
+
if key in inferred and inferred[key] != result:
|
|
78
|
+
rendered = ", ".join(f"{name}={item!r}" for name, item in zip(key_names, key))
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"lookup is not deterministic for ({rendered}): "
|
|
81
|
+
f"{inferred[key]!r} != {result!r}"
|
|
82
|
+
)
|
|
83
|
+
inferred[key] = result
|
|
84
|
+
|
|
85
|
+
ordered = sorted(
|
|
86
|
+
inferred.items(),
|
|
87
|
+
key=lambda item: tuple("" if value is None else str(value) for value in item[0]),
|
|
88
|
+
)
|
|
89
|
+
return [
|
|
90
|
+
{**dict(zip(key_names, key)), value: result}
|
|
91
|
+
for key, result in ordered
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
57
95
|
def _synthetic_value(name: str, data_type: str, index: int, randomizer: random.Random) -> Any:
|
|
58
96
|
normalized_name = name.upper()
|
|
59
97
|
normalized_type = data_type.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/io.py
RENAMED
|
File without changes
|
{mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/src/mainframe_toolkit/jcl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/mvnw.cmd
RENAMED
|
File without changes
|
{mainframe_migration_toolkit-0.2.0 → mainframe_migration_toolkit-0.3.1}/validator-java/pom.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|