mainframe-migration-toolkit 0.2.0__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.
Files changed (51) hide show
  1. mainframe_migration_toolkit-0.2.0/.claude/skills/analyze-mainframe-similarity/SKILL.md +30 -0
  2. mainframe_migration_toolkit-0.2.0/.claude/skills/migrate-mainframe-job/SKILL.md +63 -0
  3. mainframe_migration_toolkit-0.2.0/.claude/skills/validate-golden-dataset/SKILL.md +12 -0
  4. mainframe_migration_toolkit-0.2.0/.gitignore +9 -0
  5. mainframe_migration_toolkit-0.2.0/AGENTS.md +10 -0
  6. mainframe_migration_toolkit-0.2.0/CLAUDE.md +2 -0
  7. mainframe_migration_toolkit-0.2.0/PKG-INFO +16 -0
  8. mainframe_migration_toolkit-0.2.0/pyproject.toml +43 -0
  9. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/__init__.py +92 -0
  10. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/__main__.py +5 -0
  11. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/cli.py +361 -0
  12. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/cobol.py +106 -0
  13. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/copybook.py +558 -0
  14. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/errors.py +15 -0
  15. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/external.py +48 -0
  16. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/io.py +202 -0
  17. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/jcl.py +126 -0
  18. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/pipeline.py +322 -0
  19. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/sequential.py +259 -0
  20. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/similarity.py +1171 -0
  21. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/sorting.py +60 -0
  22. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/specs.py +312 -0
  23. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/synthetic.py +108 -0
  24. mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/workspace.py +133 -0
  25. mainframe_migration_toolkit-0.2.0/validator-java/.mvn/wrapper/maven-wrapper.properties +3 -0
  26. mainframe_migration_toolkit-0.2.0/validator-java/mvnw +295 -0
  27. mainframe_migration_toolkit-0.2.0/validator-java/mvnw.cmd +189 -0
  28. mainframe_migration_toolkit-0.2.0/validator-java/pom.xml +116 -0
  29. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/AvroValueFormatter.java +152 -0
  30. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/CsvTabularReader.java +111 -0
  31. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/DataFormat.java +62 -0
  32. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/Difference.java +34 -0
  33. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/InputFileSet.java +45 -0
  34. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/InputOptions.java +23 -0
  35. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/JsonReportWriter.java +40 -0
  36. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/MultiFileTabularReader.java +80 -0
  37. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/Normalization.java +16 -0
  38. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ParquetTabularReader.java +61 -0
  39. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/TabularReader.java +13 -0
  40. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/TabularReaderFactory.java +29 -0
  41. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidationOptions.java +40 -0
  42. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidationReport.java +44 -0
  43. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidationService.java +395 -0
  44. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidatorCli.java +190 -0
  45. mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValueNormalizer.java +27 -0
  46. mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/DirectoryValidationTest.java +70 -0
  47. mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/JsonReportWriterTest.java +43 -0
  48. mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/KeyedValidationTest.java +89 -0
  49. mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/ParquetValidationTest.java +146 -0
  50. mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/ValidationServiceTest.java +137 -0
  51. mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/ValidatorCliTest.java +102 -0
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: analyze-mainframe-similarity
3
+ description: Cluster COBOL and JCL source by deterministic textual similarity, inspect every proposed cluster, and add an AI verdict and concise justification to JSON and CSV outputs. Use for duplicate or near-duplicate codebase analysis.
4
+ ---
5
+
6
+ # Analyze mainframe similarity
7
+
8
+ 1. Run the deterministic scan:
9
+
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
12
+ ```
13
+
14
+ Use the requested threshold, or 80 when absent. Never edit deterministic scores or cluster membership manually.
15
+
16
+ 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
+
18
+ 3. Assign exactly one verdict per requested cluster:
19
+
20
+ - `identico`: same executable semantics; differences are non-semantic.
21
+ - `similar`: materially shared flow/business implementation but at least one real semantic variation.
22
+ - `diferente`: superficial boilerplate or chaining grouped programs with different behavior.
23
+
24
+ 4. Write the requested review JSON with a short evidence-based Portuguese justification, then apply it:
25
+
26
+ ```text
27
+ python -m mainframe_toolkit similarity apply-review similarity.json similarity-review.json --json similarity-reviewed.json --csv similarity-reviewed.csv
28
+ ```
29
+
30
+ 5. Verify both reviewed files contain `parecer_ia` and `justificativa_ia` for every row. Keep singletons as `diferente` unless direct inspection establishes a duplicate omitted by the threshold.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: migrate-mainframe-job
3
+ description: Convert one specification-led JCL job and its COBOL programs to an incrementally validated PySpark pipeline with audited in-memory handoffs and final golden-dataset equality. Use when asked to migrate, continue, repair, or finish a mainframe job in this repository.
4
+ ---
5
+
6
+ # Migrate mainframe job
7
+
8
+ Migrate `$ARGUMENTS`, or the single job requested by the user. Do not widen the job scope.
9
+
10
+ ## Preflight
11
+
12
+ 1. Install the repo package if its import fails:
13
+
14
+ ```text
15
+ python -m pip install -e ".[all]"
16
+ ```
17
+
18
+ 2. Run `python -m mainframe_toolkit inspect . --output .migration-work/project-inspection.json`. Resolve structural errors before conversion; unresolved dependencies may proceed only through the synthetic/external rules below.
19
+ 3. Read the selected `spec/jcl/<JOB>-jcl.json` and source JCL completely. Reconcile their ordered steps, conditions, DD concatenation, SORT/IDCAMS control cards, input/output datasets, GDGs, and return-code behavior. The spec defines intended business behavior; source resolves mechanics the spec leaves open.
20
+ 4. Read every referenced `spec/cobol/<PROGRAM>-cbl.json`, COBOL source, used copybook, and relevant entry in `external-programs/*.json`. Do not infer a record layout from narrative text when a copybook exists. Distinguish physical COBOL width from any expanded textual transport width.
21
+ 5. Inventory every supplied input and golden file before generating anything. Freeze clock-dependent behavior behind an injected processing date.
22
+
23
+ ## Target shape
24
+
25
+ Create only the files needed under `converted/<JOB>/`:
26
+
27
+ ```text
28
+ <job_lower>.py
29
+ programs/__init__.py
30
+ programs/<program_lower>.py
31
+ tests/...
32
+ RELATORIO_FINAL.md # only after acceptance
33
+ ```
34
+
35
+ Create one module per COBOL program. Represent utility steps separately when they implement a transformation. Every module exposes `run(context)` and returns a mapping of logical dataset names to PySpark DataFrames, or a `ProgramResult` when a return code matters.
36
+
37
+ The JCL-named entrypoint contains no transformation, validation, lookup, calculation, filtering, joining, or formatting rules. It only initializes Spark and `PipelineContext`, imports modules, declares `PipelineProgram` order/conditions, loads runtime arguments, and calls `PipelineRunner`. It must accept `--through <PROGRAM>` so prefixes are executable.
38
+
39
+ Use `mainframe_toolkit.io` for local/S3 CSV and Parquet, `sequential`/`copybook` for fixed records, `sorting` for stable multi-key sort, `external` for subprogram adapters, and COBOL decimal helpers instead of cloning generic logic into modules.
40
+
41
+ ## Ordered conversion gate
42
+
43
+ For each executable step in JCL order:
44
+
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
+ 2. Add its import and `PipelineProgram` entry to the JCL-named file immediately.
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. Keep relationships valid, use a stable seed, place it under `converted/<JOB>/synthetic/`, and write a machine-readable provenance manifest. Never overwrite or shadow supplied data.
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
+
51
+ Do not use the final golden comparison for a partial prefix unless that prefix is itself the documented final producer.
52
+
53
+ ## Final equivalence loop
54
+
55
+ 1. Execute the full pipeline with a clean run ID. Make final row ordering explicit using contract keys before export; Spark partition order is not a business order.
56
+ 2. Build the Java validator if needed, then invoke it through `python -m mainframe_toolkit golden validate ...` with the final output, matching golden file, format/delimiter, and ordering keys.
57
+ 3. Acceptance is `status=MATCH`, zero differing cells, equal row/column counts, and exit code 0.
58
+ 4. On mismatch, start from the first reported row/key/column. Trace that field backward through the per-program audit outputs and JCL data flow; identify the earliest program that creates the wrong value, row, multiplicity, or ordering. Correct that module, rerun its prefix, then rerun the full pipeline and validator.
59
+ 5. Repeat until 100% equal. Do not weaken comparison options, discard differing rows, alter the golden dataset, or encode golden values as business rules.
60
+
61
+ ## Final artifact
62
+
63
+ Only after acceptance, create `converted/<JOB>/RELATORIO_FINAL.md` with: programs converted in order; material spec/source decisions; supplied versus synthetic inputs and external adapters; tests/checkpoints executed; exact final comparison result; and the shortest commands needed to run the job and validator. Do not add deployment guidance.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: validate-golden-dataset
3
+ description: Compare a converted pipeline's final CSV or Parquet output cell by cell against its golden dataset and localize mismatches using audit lineage. Use for final equivalence checks or divergence repair.
4
+ ---
5
+
6
+ # Validate golden dataset
7
+
8
+ Build `validator-java` when its executable JAR is absent, then run `python -m mainframe_toolkit golden validate` for `$ARGUMENTS`. Supply the real delimiter/header/null/decimal options and explicit ordering keys whenever record order is not contractual.
9
+
10
+ Require equal schema, column order, row count, and normalized cell values. Exit code 0 and zero differences are the only passing result.
11
+
12
+ For a mismatch, use the report's first row/key/column and the audit manifest to trace the field backward through program outputs. Fix the earliest converted program that introduces the divergence, rerun its full prefix, then the full job and validator. Repeat without changing the golden dataset or relaxing comparison semantics.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ .tmp/
3
+ .pytest_tmp/
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ target/
@@ -0,0 +1,10 @@
1
+ # Mainframe migration toolkit
2
+
3
+ - Treat `spec/` as the intended behavior, the JCL/COBOL/copybook sources as the mechanical detail, and `golden-dataset/` as the final observable acceptance criterion. Record material conflicts in the final report.
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
+ - 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
+ - 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.
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
+ - 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
+
@@ -0,0 +1,2 @@
1
+ @AGENTS.md
2
+
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.5
2
+ Name: mainframe-migration-toolkit
3
+ Version: 0.2.0
4
+ Summary: Runtime and deterministic tools for COBOL/JCL to PySpark migrations
5
+ Author: Mainframe Migration Toolkit
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: all
8
+ Requires-Dist: pyarrow>=15; extra == 'all'
9
+ Requires-Dist: pyspark<5,>=3.5; extra == 'all'
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
12
+ Requires-Dist: pytest>=8.2; extra == 'dev'
13
+ Provides-Extra: parquet
14
+ Requires-Dist: pyarrow>=15; extra == 'parquet'
15
+ Provides-Extra: spark
16
+ Requires-Dist: pyspark<5,>=3.5; extra == 'spark'
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mainframe-migration-toolkit"
7
+ version = "0.2.0"
8
+ description = "Runtime and deterministic tools for COBOL/JCL to PySpark migrations"
9
+ requires-python = ">=3.10"
10
+ authors = [{ name = "Mainframe Migration Toolkit" }]
11
+ dependencies = []
12
+
13
+ [project.optional-dependencies]
14
+ spark = ["pyspark>=3.5,<5"]
15
+ parquet = ["pyarrow>=15"]
16
+ all = [
17
+ "pyspark>=3.5,<5",
18
+ "pyarrow>=15",
19
+ ]
20
+ dev = [
21
+ "pytest>=8.2",
22
+ "pytest-cov>=5",
23
+ ]
24
+
25
+ [project.scripts]
26
+ mainframe-toolkit = "mainframe_toolkit.cli:main"
27
+ mft = "mainframe_toolkit.cli:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/mainframe_toolkit"]
31
+
32
+ [tool.hatch.build.targets.wheel.force-include]
33
+ "AGENTS.md" = "mainframe_toolkit/_workspace/AGENTS.md"
34
+ "CLAUDE.md" = "mainframe_toolkit/_workspace/CLAUDE.md"
35
+ ".claude/skills" = "mainframe_toolkit/_workspace/.claude/skills"
36
+ "validator-java" = "mainframe_toolkit/_workspace/validator-java"
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = ["/pyproject.toml", "/src", "/AGENTS.md", "/CLAUDE.md", "/.claude/skills", "/validator-java"]
40
+
41
+ [tool.pytest.ini_options]
42
+ addopts = "-q --basetemp=.pytest_tmp"
43
+ testpaths = ["tests"]
@@ -0,0 +1,92 @@
1
+ """Reusable runtime for specification-led COBOL/JCL migrations."""
2
+
3
+ from .cobol import (
4
+ cobol_decimal,
5
+ decode_overpunch,
6
+ decode_packed_decimal,
7
+ encode_packed_decimal,
8
+ is_numeric,
9
+ quantize_cobol,
10
+ )
11
+ from .copybook import CopybookParseError, layout_to_dict, parse_copybook, parse_copybook_text
12
+ from .external import ExternalProgramRegistry, external_program
13
+ from .io import (
14
+ configure_s3,
15
+ read_csv,
16
+ read_dataset,
17
+ read_delimited_rows,
18
+ read_parquet,
19
+ read_s3_csv,
20
+ read_s3_parquet,
21
+ s3_uri,
22
+ write_csv,
23
+ write_dataset,
24
+ write_parquet,
25
+ write_s3_csv,
26
+ write_s3_parquet,
27
+ )
28
+ from .jcl import JCLJob, JCLStep, parse_jcl
29
+ from .pipeline import (
30
+ DatasetRegistry,
31
+ PipelineContext,
32
+ PipelineProgram,
33
+ ProgramResult,
34
+ PipelineRunner,
35
+ PipelineRunError,
36
+ ProgramReturnCodeError,
37
+ )
38
+ from .sequential import FieldSpec, RecordLayout, convert_sequential, iter_sequential
39
+ from .sorting import SortKey, sort_dataframe, sort_records
40
+ from .synthetic import synthetic_dataframe, synthetic_rows
41
+ from .workspace import WorkspaceInitResult, init_workspace
42
+
43
+ __all__ = [
44
+ "CopybookParseError",
45
+ "DatasetRegistry",
46
+ "ExternalProgramRegistry",
47
+ "FieldSpec",
48
+ "JCLJob",
49
+ "JCLStep",
50
+ "PipelineContext",
51
+ "PipelineProgram",
52
+ "ProgramResult",
53
+ "ProgramReturnCodeError",
54
+ "PipelineRunError",
55
+ "PipelineRunner",
56
+ "RecordLayout",
57
+ "SortKey",
58
+ "WorkspaceInitResult",
59
+ "cobol_decimal",
60
+ "configure_s3",
61
+ "convert_sequential",
62
+ "decode_overpunch",
63
+ "decode_packed_decimal",
64
+ "encode_packed_decimal",
65
+ "external_program",
66
+ "is_numeric",
67
+ "init_workspace",
68
+ "iter_sequential",
69
+ "layout_to_dict",
70
+ "parse_copybook",
71
+ "parse_copybook_text",
72
+ "parse_jcl",
73
+ "quantize_cobol",
74
+ "read_csv",
75
+ "read_dataset",
76
+ "read_delimited_rows",
77
+ "read_parquet",
78
+ "read_s3_csv",
79
+ "read_s3_parquet",
80
+ "s3_uri",
81
+ "sort_dataframe",
82
+ "sort_records",
83
+ "synthetic_dataframe",
84
+ "synthetic_rows",
85
+ "write_csv",
86
+ "write_dataset",
87
+ "write_parquet",
88
+ "write_s3_csv",
89
+ "write_s3_parquet",
90
+ ]
91
+
92
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ raise SystemExit(main())
5
+
@@ -0,0 +1,361 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import csv
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, Sequence
13
+
14
+ from .jcl import parse_jcl
15
+ from .sequential import RecordLayout, convert_sequential
16
+ from .similarity import (
17
+ analyze_codebase,
18
+ apply_ai_reviews,
19
+ write_ai_review_payload,
20
+ write_results,
21
+ )
22
+ from .specs import inspect_project, write_inspection
23
+ from .synthetic import synthetic_rows
24
+ from .workspace import init_workspace
25
+
26
+
27
+ def _json_dump(value: Any, path: str | Path | None = None) -> None:
28
+ content = json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n"
29
+ if path is None or str(path) == "-":
30
+ sys.stdout.write(content)
31
+ return
32
+ output = Path(path)
33
+ output.parent.mkdir(parents=True, exist_ok=True)
34
+ output.write_text(content, encoding="utf-8")
35
+
36
+
37
+ def _inspect(args: argparse.Namespace) -> int:
38
+ result = inspect_project(args.project, strict=args.strict)
39
+ if args.output:
40
+ write_inspection(result, args.output)
41
+ else:
42
+ _json_dump(result.to_dict())
43
+ return 0 if result.valid else 2
44
+
45
+
46
+ def _init_workspace(args: argparse.Namespace) -> int:
47
+ result = init_workspace(args.destination, force=args.force)
48
+ _json_dump(result.to_dict())
49
+ return 0
50
+
51
+
52
+ def _jcl_parse(args: argparse.Namespace) -> int:
53
+ _json_dump(parse_jcl(args.source).to_dict(), args.output)
54
+ return 0
55
+
56
+
57
+ def _copybook_parse(args: argparse.Namespace) -> int:
58
+ from .copybook import layout_to_dict, parse_copybook
59
+
60
+ parsed = parse_copybook(args.source)
61
+ _json_dump(layout_to_dict(parsed), args.output)
62
+ return 0
63
+
64
+
65
+ def _sequential_convert(args: argparse.Namespace) -> int:
66
+ layout = RecordLayout.from_json(args.layout)
67
+ result = convert_sequential(
68
+ args.source,
69
+ args.destination,
70
+ layout,
71
+ output_format=args.format,
72
+ mode=args.mode,
73
+ delimiter=args.delimiter,
74
+ on_error=args.on_error,
75
+ batch_size=args.batch_size,
76
+ )
77
+ _json_dump(result)
78
+ return 0
79
+
80
+
81
+ def _similarity_scan(args: argparse.Namespace) -> int:
82
+ report = analyze_codebase(
83
+ args.root,
84
+ threshold=args.threshold,
85
+ shingle_size=args.shingle_size,
86
+ exhaustive_limit=args.exhaustive_limit,
87
+ )
88
+ write_results(report, args.json, args.csv)
89
+ write_ai_review_payload(report, args.review_request)
90
+ _json_dump(
91
+ {
92
+ **report.statistics,
93
+ "json": str(Path(args.json)),
94
+ "csv": str(Path(args.csv)),
95
+ "review_request": str(Path(args.review_request)),
96
+ }
97
+ )
98
+ return 0
99
+
100
+
101
+ def _similarity_apply(args: argparse.Namespace) -> int:
102
+ json_output = args.json or args.report
103
+ csv_output = args.csv or str(Path(json_output).with_suffix(".csv"))
104
+ result = apply_ai_reviews(
105
+ args.report,
106
+ args.reviews,
107
+ json_path=json_output,
108
+ csv_path=csv_output,
109
+ )
110
+ records = result.records if hasattr(result, "records") else result.get("arquivos", [])
111
+ missing = sum(not row.get("parecer_ia") or not row.get("justificativa_ia") for row in records)
112
+ _json_dump({"json": json_output, "csv": csv_output, "rows": len(records), "reviews_missing": missing})
113
+ return 0 if missing == 0 else 1
114
+
115
+
116
+ def _synthetic(args: argparse.Namespace) -> int:
117
+ payload = json.loads(Path(args.schema).read_text(encoding="utf-8"))
118
+ schema = payload.get("schema", payload) if isinstance(payload, dict) else payload
119
+ if not isinstance(schema, dict) or not all(isinstance(value, str) for value in schema.values()):
120
+ raise ValueError("schema must be a JSON object mapping field names to types")
121
+ rows = synthetic_rows(schema, count=args.rows, seed=args.seed)
122
+ output = Path(args.output)
123
+ output.parent.mkdir(parents=True, exist_ok=True)
124
+ if args.format == "jsonl":
125
+ with output.open("w", encoding="utf-8") as stream:
126
+ for row in rows:
127
+ stream.write(json.dumps(row, ensure_ascii=False, default=str) + "\n")
128
+ else:
129
+ with output.open("w", encoding="utf-8", newline="") as stream:
130
+ writer = csv.DictWriter(stream, fieldnames=list(schema), delimiter=args.delimiter)
131
+ writer.writeheader()
132
+ writer.writerows(rows)
133
+ digest = hashlib.sha256(output.read_bytes()).hexdigest()
134
+ manifest = Path(args.manifest) if args.manifest else output.with_suffix(output.suffix + ".manifest.json")
135
+ _json_dump(
136
+ {
137
+ "synthetic": True,
138
+ "output": str(output),
139
+ "format": args.format,
140
+ "rows": len(rows),
141
+ "seed": args.seed,
142
+ "schema": schema,
143
+ "sha256": digest,
144
+ },
145
+ manifest,
146
+ )
147
+ _json_dump({"output": str(output), "manifest": str(manifest), "rows": len(rows), "seed": args.seed})
148
+ return 0
149
+
150
+
151
+ def _toolkit_root() -> Path:
152
+ override = os.environ.get("MAINFRAME_TOOLKIT_ROOT")
153
+ if override:
154
+ return Path(override).resolve()
155
+ candidate = Path(__file__).resolve().parents[2]
156
+ return candidate if (candidate / "validator-java" / "pom.xml").is_file() else Path.cwd()
157
+
158
+
159
+ def _validator_dir(value: str | None) -> Path:
160
+ return Path(value).resolve() if value else _toolkit_root() / "validator-java"
161
+
162
+
163
+ def _maven_executable(directory: Path) -> str:
164
+ wrapper = directory / ("mvnw.cmd" if os.name == "nt" else "mvnw")
165
+ if wrapper.is_file():
166
+ return str(wrapper)
167
+ executable = shutil.which("mvn") or shutil.which("mvn.cmd")
168
+ if not executable:
169
+ raise RuntimeError("Maven 3.9+ or the validator-java Maven wrapper is required")
170
+ return executable
171
+
172
+
173
+ def _golden_build(args: argparse.Namespace) -> int:
174
+ directory = _validator_dir(args.validator_dir)
175
+ pom = directory / "pom.xml"
176
+ if not pom.is_file():
177
+ raise FileNotFoundError(pom)
178
+ command = [_maven_executable(directory), "-q", "-f", str(pom), "package"]
179
+ if args.skip_tests:
180
+ command.insert(-1, "-DskipTests")
181
+ process_environment = None
182
+ java_home = getattr(args, "java_home", None)
183
+ if java_home:
184
+ process_environment = os.environ.copy()
185
+ process_environment["JAVA_HOME"] = str(Path(java_home).resolve())
186
+ return subprocess.run(command, check=False, env=process_environment).returncode
187
+
188
+
189
+ def _resolve_validator_jar(args: argparse.Namespace) -> Path:
190
+ if args.jar:
191
+ jar = Path(args.jar).resolve()
192
+ elif os.environ.get("MAINFRAME_VALIDATOR_JAR"):
193
+ jar = Path(os.environ["MAINFRAME_VALIDATOR_JAR"]).resolve()
194
+ else:
195
+ jar = _validator_dir(args.validator_dir) / "target" / "dataset-validator.jar"
196
+ if not jar.is_file() and args.build:
197
+ code = _golden_build(
198
+ argparse.Namespace(
199
+ validator_dir=args.validator_dir,
200
+ skip_tests=False,
201
+ java_home=(str(Path(args.java).resolve().parent.parent) if args.java else None),
202
+ )
203
+ )
204
+ if code != 0:
205
+ raise RuntimeError(f"validator-java build failed with exit code {code}")
206
+ if not jar.is_file():
207
+ raise FileNotFoundError(jar)
208
+ return jar
209
+
210
+
211
+ def _golden_validate(args: argparse.Namespace) -> int:
212
+ requested_java = args.java or os.environ.get("MAINFRAME_JAVA")
213
+ java = str(Path(requested_java).resolve()) if requested_java else shutil.which("java")
214
+ if not java:
215
+ raise RuntimeError("Java 17+ is required to run the dataset validator")
216
+ command = [java, "-jar", str(_resolve_validator_jar(args)), args.expected, args.actual]
217
+ scalar_options = {
218
+ "--format": args.format,
219
+ "--expected-format": args.expected_format,
220
+ "--actual-format": args.actual_format,
221
+ "--delimiter": args.delimiter,
222
+ "--expected-delimiter": args.expected_delimiter,
223
+ "--actual-delimiter": args.actual_delimiter,
224
+ "--header": args.header,
225
+ "--expected-header": args.expected_header,
226
+ "--actual-header": args.actual_header,
227
+ "--charset": args.charset,
228
+ "--max-differences": args.max_differences,
229
+ "--report": args.report,
230
+ }
231
+ for option, value in scalar_options.items():
232
+ if value is not None:
233
+ command.extend((option, str(value)))
234
+ if args.no_header:
235
+ command.append("--no-header")
236
+ if args.trim:
237
+ command.append("--trim")
238
+ if args.normalize_decimal:
239
+ command.append("--normalize-decimal")
240
+ for value in args.normalize:
241
+ command.extend(("--normalize", value))
242
+ for value in args.key:
243
+ command.extend(("--key", value))
244
+ return subprocess.run(command, check=False).returncode
245
+
246
+
247
+ def _parser() -> argparse.ArgumentParser:
248
+ parser = argparse.ArgumentParser(prog="mainframe-toolkit")
249
+ subcommands = parser.add_subparsers(dest="command", required=True)
250
+
251
+ initialize = subcommands.add_parser("init-workspace", help="materialize a migration workspace")
252
+ initialize.add_argument("destination", nargs="?", default=".")
253
+ initialize.add_argument(
254
+ "--force",
255
+ action="store_true",
256
+ help="update conflicting toolkit-managed files without replacing project inputs",
257
+ )
258
+ initialize.set_defaults(handler=_init_workspace)
259
+
260
+ inspect = subcommands.add_parser("inspect", help="validate the migration project contract")
261
+ inspect.add_argument("project", nargs="?", default=".")
262
+ inspect.add_argument("--output", "-o")
263
+ inspect.add_argument("--strict", action="store_true")
264
+ inspect.set_defaults(handler=_inspect)
265
+
266
+ jcl = subcommands.add_parser("jcl", help="JCL source tools").add_subparsers(dest="jcl_command", required=True)
267
+ jcl_parse = jcl.add_parser("parse")
268
+ jcl_parse.add_argument("source")
269
+ jcl_parse.add_argument("--output", "-o")
270
+ jcl_parse.set_defaults(handler=_jcl_parse)
271
+
272
+ copybook = subcommands.add_parser("copybook", help="copybook layout tools").add_subparsers(dest="copybook_command", required=True)
273
+ copybook_parse = copybook.add_parser("parse")
274
+ copybook_parse.add_argument("source")
275
+ copybook_parse.add_argument("--output", "-o")
276
+ copybook_parse.set_defaults(handler=_copybook_parse)
277
+
278
+ sequential = subcommands.add_parser("sequential", help="sequential dataset tools").add_subparsers(dest="sequential_command", required=True)
279
+ convert = sequential.add_parser("convert")
280
+ convert.add_argument("source")
281
+ convert.add_argument("destination")
282
+ convert.add_argument("--layout", required=True)
283
+ convert.add_argument("--format", choices=("csv", "parquet"))
284
+ convert.add_argument("--mode", choices=("line", "fixed"), default="line")
285
+ convert.add_argument("--delimiter", default=",")
286
+ convert.add_argument("--on-error", choices=("raise", "skip", "null"), default="raise")
287
+ convert.add_argument("--batch-size", type=int, default=10_000)
288
+ convert.set_defaults(handler=_sequential_convert)
289
+
290
+ similarity = subcommands.add_parser("similarity", help="deterministic source clustering").add_subparsers(dest="similarity_command", required=True)
291
+ scan = similarity.add_parser("scan")
292
+ scan.add_argument("root")
293
+ scan.add_argument("--threshold", type=float, default=80.0)
294
+ scan.add_argument("--shingle-size", type=int, default=3)
295
+ scan.add_argument("--exhaustive-limit", type=int, default=80)
296
+ scan.add_argument("--json", default="similarity.json")
297
+ scan.add_argument("--csv", default="similarity.csv")
298
+ scan.add_argument("--review-request", default="similarity-review-request.json")
299
+ scan.set_defaults(handler=_similarity_scan)
300
+ review = similarity.add_parser("apply-review")
301
+ review.add_argument("report")
302
+ review.add_argument("reviews")
303
+ review.add_argument("--json")
304
+ review.add_argument("--csv")
305
+ review.set_defaults(handler=_similarity_apply)
306
+
307
+ synthetic = subcommands.add_parser("synthetic", help="create deterministic fallback rows")
308
+ synthetic.add_argument("--schema", required=True)
309
+ synthetic.add_argument("--output", required=True)
310
+ synthetic.add_argument("--manifest")
311
+ synthetic.add_argument("--rows", type=int, default=10)
312
+ synthetic.add_argument("--seed", default="0")
313
+ synthetic.add_argument("--format", choices=("jsonl", "csv"), default="jsonl")
314
+ synthetic.add_argument("--delimiter", default=",")
315
+ synthetic.set_defaults(handler=_synthetic)
316
+
317
+ golden = subcommands.add_parser("golden", help="Java cell-level dataset validator").add_subparsers(dest="golden_command", required=True)
318
+ build = golden.add_parser("build")
319
+ build.add_argument("--validator-dir")
320
+ build.add_argument("--java-home")
321
+ build.add_argument("--skip-tests", action="store_true")
322
+ build.set_defaults(handler=_golden_build)
323
+ validate = golden.add_parser("validate")
324
+ validate.add_argument("expected")
325
+ validate.add_argument("actual")
326
+ validate.add_argument("--jar")
327
+ validate.add_argument("--java")
328
+ validate.add_argument("--validator-dir")
329
+ validate.add_argument("--build", action=argparse.BooleanOptionalAction, default=True)
330
+ validate.add_argument("--format", choices=("auto", "csv", "parquet"), default="auto")
331
+ validate.add_argument("--expected-format", choices=("auto", "csv", "parquet"))
332
+ validate.add_argument("--actual-format", choices=("auto", "csv", "parquet"))
333
+ validate.add_argument("--delimiter", default=",")
334
+ validate.add_argument("--expected-delimiter")
335
+ validate.add_argument("--actual-delimiter")
336
+ validate.add_argument("--no-header", action="store_true")
337
+ validate.add_argument("--header", choices=("true", "false"))
338
+ validate.add_argument("--expected-header", choices=("true", "false"))
339
+ validate.add_argument("--actual-header", choices=("true", "false"))
340
+ validate.add_argument("--charset", default="UTF-8")
341
+ validate.add_argument("--normalize", action="append", default=[])
342
+ validate.add_argument("--trim", action="store_true")
343
+ validate.add_argument("--normalize-decimal", action="store_true")
344
+ validate.add_argument("--key", action="append", default=[])
345
+ validate.add_argument("--max-differences", type=int, default=100)
346
+ validate.add_argument("--report", "-r", default="-")
347
+ validate.set_defaults(handler=_golden_validate)
348
+ return parser
349
+
350
+
351
+ def main(argv: Sequence[str] | None = None) -> int:
352
+ try:
353
+ args = _parser().parse_args(argv)
354
+ return int(args.handler(args))
355
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
356
+ sys.stderr.write(f"error: {exc}\n")
357
+ return 2
358
+
359
+
360
+ if __name__ == "__main__":
361
+ raise SystemExit(main())