From a4e8a325a22739e8cec16af388e60157902958a9 Mon Sep 17 00:00:00 2001 From: ncantu Date: Mon, 9 Mar 2026 00:42:41 +0100 Subject: [PATCH] Generalize local H6 run reports with a parameterized profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivations:** - Avoid one-profile-per-state duplication for local H6 completeness checks - Reuse a single strict verifier for any versioned local H6 artefacts set **Root causes:** - The initial local_H6_E1 profile was state-specific and not reusable for other E **Correctifs:** - Factor local H6 verification into a generic union-vs-lift equality check **Evolutions:** - Add `local_H6` profile with `--local-h6-artefacts-dir` - Keep `local_H6_E1` as a stable alias to the E1 artefacts directory - Document the generalized usage and update the E1 report output **Pages affectées:** - applications/collatz/collatz_k_scripts/collatz_generate_run_report.py - docs/collatz_run_report_format.md - docs/features/collatz_run_report_generator.md - docs/collatz_run_report_2026-03-09_local_H6_E1.md --- .../collatz_generate_run_report.py | 187 ++++++++++-------- ...llatz_run_report_2026-03-09_local_H6_E1.md | 8 +- docs/collatz_run_report_format.md | 11 ++ docs/features/collatz_run_report_generator.md | 3 +- 4 files changed, 121 insertions(+), 88 deletions(-) diff --git a/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py b/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py index 9de5f92..7d0ef43 100644 --- a/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py +++ b/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py @@ -109,51 +109,74 @@ class Fusion25Metrics: @dataclass(frozen=True) -class LocalH6E1Metrics: +class LocalH6Metrics: base_palier: int base_domain_size: int target_palier: int lifted_domain_size: int - d8_covered: int - f9to40_covered: int union_covered: int + certificate_coverages: list[tuple[str, int]] -def compute_local_h6_e1_metrics( +def _discover_base_noyau_path(artefacts_dir: Path) -> Path: + candidates = sorted((artefacts_dir / "noyaux").glob("noyau_*_B12.json")) + if len(candidates) != 1: + raise ValueError( + "Cannot auto-detect base noyau file: expected exactly one match " + f"for {artefacts_dir / 'noyaux' / 'noyau_*_B12.json'}" + ) + return candidates[0] + + +def _discover_certificate_paths(artefacts_dir: Path) -> list[Path]: + paths = sorted((artefacts_dir / "certificats").glob("certificat_*.json")) + if not paths: + raise ValueError(f"No certificate JSON found under {artefacts_dir / 'certificats'}") + return paths + + +def compute_local_h6_metrics( *, - noyau_e1_b12_path: Path, - certificat_d8_path: Path, - certificat_f9to40_path: Path, -) -> LocalH6E1Metrics: - noyau_obj = read_json(noyau_e1_b12_path) + noyau_base_path: Path, + certificate_paths: list[Path], +) -> LocalH6Metrics: + noyau_obj = read_json(noyau_base_path) if not isinstance(noyau_obj, dict): - raise ValueError("Invalid JSON: noyau_E1_B12.json must be an object") + raise ValueError("Invalid JSON: base noyau must be an object") noyau = noyau_obj.get("noyau") base_palier = noyau_obj.get("palier") if not isinstance(noyau, list) or not all(isinstance(x, int) for x in noyau): - raise ValueError("Invalid JSON: noyau_E1_B12.json must contain an integer list field 'noyau'") + raise ValueError("Invalid JSON: base noyau must contain an integer list field 'noyau'") if not isinstance(base_palier, int): - raise ValueError("Invalid JSON: noyau_E1_B12.json must contain an integer field 'palier'") + raise ValueError("Invalid JSON: base noyau must contain an integer field 'palier'") - d8_obj = read_json(certificat_d8_path) - f_obj = read_json(certificat_f9to40_path) - if not isinstance(d8_obj, dict) or not isinstance(f_obj, dict): - raise ValueError("Invalid JSON: certificates must be objects") + if not certificate_paths: + raise ValueError("compute_local_h6_metrics requires at least one certificate path") - d8_cov = d8_obj.get("covered") - f_cov = f_obj.get("covered") - if not isinstance(d8_cov, list) or not all(isinstance(x, int) for x in d8_cov): - raise ValueError("Invalid JSON: certificat_D8 must contain an integer list field 'covered'") - if not isinstance(f_cov, list) or not all(isinstance(x, int) for x in f_cov): - raise ValueError("Invalid JSON: certificat_F9to40 must contain an integer list field 'covered'") + palier: int | None = None + covered_union: set[int] = set() + certificate_coverages: list[tuple[str, int]] = [] + for cert_path in certificate_paths: + cert_obj = read_json(cert_path) + if not isinstance(cert_obj, dict): + raise ValueError(f"Invalid JSON: certificate must be an object: {cert_path}") + cov = cert_obj.get("covered") + if not isinstance(cov, list) or not all(isinstance(x, int) for x in cov): + raise ValueError(f"Invalid JSON: certificate must contain an integer list field 'covered': {cert_path}") + cert_palier = cert_obj.get("palier") + if not isinstance(cert_palier, int): + raise ValueError(f"Invalid JSON: certificate must contain an integer field 'palier': {cert_path}") + if palier is None: + palier = cert_palier + elif cert_palier != palier: + raise ValueError(f"Certificate palier mismatch: {cert_path} has palier={cert_palier}, expected {palier}") + s = set(cov) + covered_union |= s + certificate_coverages.append((str(cert_path), len(s))) - d8_palier = d8_obj.get("palier") - f_palier = f_obj.get("palier") - if not isinstance(d8_palier, int) or not isinstance(f_palier, int): - raise ValueError("Invalid JSON: certificates must contain an integer field 'palier'") - if d8_palier != f_palier: - raise ValueError(f"Certificate palier mismatch: D8={d8_palier}, F9to40={f_palier}") - target_palier = d8_palier + if palier is None: + raise ValueError("Cannot infer target palier from certificates") + target_palier = palier if target_palier < base_palier: raise ValueError(f"Target palier must be >= base palier (target={target_palier}, base={base_palier})") @@ -166,25 +189,20 @@ def compute_local_h6_e1_metrics( for j in range(lift_factor): lifted_domain.add(r + j * lift_step) - d8_set = set(d8_cov) - f_set = set(f_cov) - union = d8_set | f_set - - missing = lifted_domain - union - extra = union - lifted_domain + missing = lifted_domain - covered_union + extra = covered_union - lifted_domain if missing: - raise ValueError(f"Local H6(E1) check failed: missing {len(missing)} lifted residues") + raise ValueError(f"Local H6 check failed: missing {len(missing)} lifted residues") if extra: - raise ValueError(f"Local H6(E1) check failed: extra {len(extra)} residues outside lifted domain") + raise ValueError(f"Local H6 check failed: extra {len(extra)} residues outside lifted domain") - return LocalH6E1Metrics( + return LocalH6Metrics( base_palier=base_palier, base_domain_size=len(noyau), target_palier=target_palier, lifted_domain_size=len(lifted_domain), - d8_covered=len(d8_set), - f9to40_covered=len(f_set), - union_covered=len(union), + union_covered=len(covered_union), + certificate_coverages=certificate_coverages, ) @@ -579,7 +597,7 @@ def write_local_h6_e1_run_report( report_title: str, command: str, sha_entries: list[Sha256Entry], - metrics: LocalH6E1Metrics, + metrics: LocalH6Metrics, artefacts_dir: Path, ) -> None: lines: list[str] = [] @@ -612,9 +630,9 @@ def write_local_h6_e1_run_report( lines.append(f"- |B12(E1)| : {metrics.base_domain_size}") lines.append(f"- palier cible : {metrics.target_palier}") lines.append(f"- |Lift| : {metrics.lifted_domain_size}") - lines.append(f"- |covered(D8)| : {metrics.d8_covered}") - lines.append(f"- |covered(F9–F40)| : {metrics.f9to40_covered}") - lines.append(f"- |covered(D8 ∪ F9–F40)| : {metrics.union_covered}") + for path_str, c in metrics.certificate_coverages: + lines.append(f"- |covered({Path(path_str).name})| : {c}") + lines.append(f"- |covered(∪ certificats)| : {metrics.union_covered}") lines.append("") lines.append("## Chemins d’artefacts (versionnés)") lines.append("") @@ -640,7 +658,7 @@ def main() -> None: ap.add_argument( "--profile", default="extend_finale", - choices=["extend_finale", "validation_section7", "pipeline_d16_d17", "fusion_palier2p25", "local_H6_E1"], + choices=["extend_finale", "validation_section7", "pipeline_d16_d17", "fusion_palier2p25", "local_H6", "local_H6_E1"], help="Report profile", ) ap.add_argument("--pipeline-extend-log", default=None, help="Path to pipeline_extend.log (defaults to OUT/pipeline_extend.log)") @@ -651,6 +669,11 @@ def main() -> None: default="", help="Command line to embed in report (profile-dependent default if empty)", ) + ap.add_argument( + "--local-h6-artefacts-dir", + default="", + help="For profile local_H6: path to the versioned artefacts directory (e.g. docs/artefacts/collatz/local_E1_palier2p13)", + ) args = ap.parse_args() repo_root = Path(args.repo_root).resolve() if args.repo_root else Path.cwd().resolve() @@ -813,46 +836,44 @@ def main() -> None: print(f"Wrote: {output_path}") return - if args.profile == "local_H6_E1": - command = ( - args.command.strip() - if args.command.strip() - else "python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py --profile local_H6_E1 --scope local_H6_E1 --out-dir applications/collatz/out --docs-dir docs" - ) - artefacts_dir = docs_dir / "artefacts" / "collatz" / "local_E1_palier2p13" - noyau_e1_b12_path = artefacts_dir / "noyaux" / "noyau_E1_B12.json" - noyau_post_d8_path = artefacts_dir / "noyaux" / "noyau_post_D8_E1_palier2p13.json" - candidats_d8_path = artefacts_dir / "candidats" / "candidats_D8_E1_palier2p13.csv" - candidats_f9to40_path = artefacts_dir / "candidats" / "candidats_F9to40_E1_palier2p13.csv" - certificat_d8_path = artefacts_dir / "certificats" / "certificat_D8_E1_palier2p13.json" - certificat_f9to40_path = artefacts_dir / "certificats" / "certificat_F9to40_E1_palier2p13.json" - audit_md_path = artefacts_dir / "audits" / "verification_H6_E1_palier2p13.md" + if args.profile in ("local_H6", "local_H6_E1"): + command = args.command.strip() + if not command: + if args.profile == "local_H6_E1": + command = ( + "python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py " + "--profile local_H6_E1 --scope local_H6_E1 --out-dir applications/collatz/out --docs-dir docs" + ) + else: + command = ( + "python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py " + "--profile local_H6 --scope local_H6 --out-dir applications/collatz/out --docs-dir docs " + "--local-h6-artefacts-dir " + ) - metrics = compute_local_h6_e1_metrics( - noyau_e1_b12_path=noyau_e1_b12_path, - certificat_d8_path=certificat_d8_path, - certificat_f9to40_path=certificat_f9to40_path, - ) - date_str = pick_report_date_from_mtime( - [ - noyau_e1_b12_path, - noyau_post_d8_path, - candidats_d8_path, - candidats_f9to40_path, - certificat_d8_path, - certificat_f9to40_path, - audit_md_path, - ] - ) + if args.profile == "local_H6_E1": + artefacts_dir = docs_dir / "artefacts" / "collatz" / "local_E1_palier2p13" + else: + if not args.local_h6_artefacts_dir.strip(): + raise ValueError("--local-h6-artefacts-dir is required for profile local_H6") + artefacts_dir = Path(args.local_h6_artefacts_dir).resolve() + + noyau_base_path = _discover_base_noyau_path(artefacts_dir) + certificate_paths = _discover_certificate_paths(artefacts_dir) + metrics = compute_local_h6_metrics(noyau_base_path=noyau_base_path, certificate_paths=certificate_paths) + + all_files: list[Path] = [] + for sub in ("noyaux", "candidats", "certificats", "audits"): + p = artefacts_dir / sub + if p.exists(): + all_files.extend(sorted([f for f in p.glob("*") if f.is_file()])) + if not all_files: + raise ValueError(f"No artefact files found under {artefacts_dir}") + + date_str = pick_report_date_from_mtime(all_files) sha_paths: list[Path] = [ repo_root / "applications" / "collatz" / "collatz_k_scripts" / "collatz_generate_run_report.py", - noyau_e1_b12_path, - noyau_post_d8_path, - candidats_d8_path, - candidats_f9to40_path, - certificat_d8_path, - certificat_f9to40_path, - audit_md_path, + *all_files, ] sha_entries = compute_sha256_entries(sha_paths) output_path = docs_dir / f"collatz_run_report_{date_str}_{args.scope}.md" diff --git a/docs/collatz_run_report_2026-03-09_local_H6_E1.md b/docs/collatz_run_report_2026-03-09_local_H6_E1.md index 46750cf..e570f02 100644 --- a/docs/collatz_run_report_2026-03-09_local_H6_E1.md +++ b/docs/collatz_run_report_2026-03-09_local_H6_E1.md @@ -19,7 +19,7 @@ python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py -- ## Empreintes sha256 (scripts, artefacts) - `/home/ncantu/code/algo/applications/collatz/collatz_k_scripts/collatz_generate_run_report.py` - - sha256: `23734fe5f6bd2e97c16546d89c4c7b94ab9a783da3b1f7d45d8bf115c9de7658` + - sha256: `b51cab5d56ab63ec864b92f3693edde8aaaa80e356b3fa0f023f69417b918296` - `/home/ncantu/code/algo/docs/artefacts/collatz/local_E1_palier2p13/noyaux/noyau_E1_B12.json` - sha256: `7f6bb5e8893ae7456f54963491a3eb80d03a7cff3f87feb12702788e43661875` - `/home/ncantu/code/algo/docs/artefacts/collatz/local_E1_palier2p13/noyaux/noyau_post_D8_E1_palier2p13.json` @@ -41,9 +41,9 @@ python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py -- - |B12(E1)| : 16 - palier cible : 13 - |Lift| : 32 -- |covered(D8)| : 2 -- |covered(F9–F40)| : 30 -- |covered(D8 ∪ F9–F40)| : 32 +- |covered(certificat_D8_E1_palier2p13.json)| : 2 +- |covered(certificat_F9to40_E1_palier2p13.json)| : 30 +- |covered(∪ certificats)| : 32 ## Chemins d’artefacts (versionnés) diff --git a/docs/collatz_run_report_format.md b/docs/collatz_run_report_format.md index 9b914a7..51ba048 100644 --- a/docs/collatz_run_report_format.md +++ b/docs/collatz_run_report_format.md @@ -67,6 +67,17 @@ python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py \ --docs-dir docs ``` +Pour une complétude locale H6 généralisée (artefacts versionnés, répertoire paramétrable) : + +```bash +python3 applications/collatz/collatz_k_scripts/collatz_generate_run_report.py \ + --profile local_H6 \ + --scope local_H6_E42 \ + --local-h6-artefacts-dir docs/artefacts/collatz/local_E42_palier2p13 \ + --out-dir applications/collatz/out \ + --docs-dir docs +``` + ### Contexte - **But du run** : (énoncé court) diff --git a/docs/features/collatz_run_report_generator.md b/docs/features/collatz_run_report_generator.md index 390b4b1..36bb237 100644 --- a/docs/features/collatz_run_report_generator.md +++ b/docs/features/collatz_run_report_generator.md @@ -23,13 +23,14 @@ afin d’éviter toute insertion de transcript terminal dans les documents math - Ajout d’un format standard : `docs/collatz_run_report_format.md`. - Ajout d’un générateur : `applications/collatz/collatz_k_scripts/collatz_generate_run_report.py`. - Ajout d’un exemple réel de rapport : `docs/collatz_run_report_2026-03-04_extend_D18_D21_resume_from_D20.md`. - - Profils supportés : `extend_finale`, `validation_section7`, `pipeline_d16_d17`, `fusion_palier2p25`, `local_H6_E1`. + - Profils supportés : `extend_finale`, `validation_section7`, `pipeline_d16_d17`, `fusion_palier2p25`, `local_H6_E1`, `local_H6`. ## Modalités d’analyse - Vérifier que `applications/collatz/out/pipeline_extend.log` et `applications/collatz/out/paliers_finale.log` existent et contiennent un run complet. - Vérifier la présence des artefacts listés (noyaux/candidats/certificats) dans `applications/collatz/out/`. - Pour `local_H6_E1`, vérifier la présence des artefacts versionnés dans `docs/artefacts/collatz/local_E1_palier2p13/`. + - Pour `local_H6`, fournir `--local-h6-artefacts-dir` et vérifier la présence d’un noyau base `noyaux/noyau_*_B12.json` et d’au moins un certificat `certificats/certificat_*.json`. ## Modalités de déploiement