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.
- mainframe_migration_toolkit-0.2.0/.claude/skills/analyze-mainframe-similarity/SKILL.md +30 -0
- mainframe_migration_toolkit-0.2.0/.claude/skills/migrate-mainframe-job/SKILL.md +63 -0
- mainframe_migration_toolkit-0.2.0/.claude/skills/validate-golden-dataset/SKILL.md +12 -0
- mainframe_migration_toolkit-0.2.0/.gitignore +9 -0
- mainframe_migration_toolkit-0.2.0/AGENTS.md +10 -0
- mainframe_migration_toolkit-0.2.0/CLAUDE.md +2 -0
- mainframe_migration_toolkit-0.2.0/PKG-INFO +16 -0
- mainframe_migration_toolkit-0.2.0/pyproject.toml +43 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/__init__.py +92 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/__main__.py +5 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/cli.py +361 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/cobol.py +106 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/copybook.py +558 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/errors.py +15 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/external.py +48 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/io.py +202 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/jcl.py +126 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/pipeline.py +322 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/sequential.py +259 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/similarity.py +1171 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/sorting.py +60 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/specs.py +312 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/synthetic.py +108 -0
- mainframe_migration_toolkit-0.2.0/src/mainframe_toolkit/workspace.py +133 -0
- mainframe_migration_toolkit-0.2.0/validator-java/.mvn/wrapper/maven-wrapper.properties +3 -0
- mainframe_migration_toolkit-0.2.0/validator-java/mvnw +295 -0
- mainframe_migration_toolkit-0.2.0/validator-java/mvnw.cmd +189 -0
- mainframe_migration_toolkit-0.2.0/validator-java/pom.xml +116 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/AvroValueFormatter.java +152 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/CsvTabularReader.java +111 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/DataFormat.java +62 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/Difference.java +34 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/InputFileSet.java +45 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/InputOptions.java +23 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/JsonReportWriter.java +40 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/MultiFileTabularReader.java +80 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/Normalization.java +16 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ParquetTabularReader.java +61 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/TabularReader.java +13 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/TabularReaderFactory.java +29 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidationOptions.java +40 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidationReport.java +44 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidationService.java +395 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValidatorCli.java +190 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/main/java/io/mainframe/migration/validator/ValueNormalizer.java +27 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/DirectoryValidationTest.java +70 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/JsonReportWriterTest.java +43 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/KeyedValidationTest.java +89 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/ParquetValidationTest.java +146 -0
- mainframe_migration_toolkit-0.2.0/validator-java/src/test/java/io/mainframe/migration/validator/ValidationServiceTest.java +137 -0
- 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,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,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,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())
|