# Allowlist for the public spectraMR export. FAIL-CLOSED: a tracked path ships
# only if a pattern below names it. Every pattern carries the reason it ships.
#
# A trailing "/" means "directory prefix"; anything else is an fnmatch glob.
# A pattern that matches nothing is a DEAD ALLOWANCE and must be deleted --
# export_public_tree.py --strict exits 2 on one.
#
# The roots deliberately absent are listed at the foot of this file, so a
# missing directory is always a recorded decision, never an oversight.

# ---- the package -----------------------------------------------------------
src/                       # the framework itself

# ---- tests -----------------------------------------------------------------
# Ships wholesale, minus the denials below. A gate that can no longer see its
# corpus must be REMOVED, not left to pass vacuously -- a repo-rooted script run
# from outside its repo exits 0 all-clean, and that reads as a pass.
tests/
conftest.py

# ---- tests: denials ---------------------------------------------------------
# `!` denials, one file per line. A path ships iff an allowance names it AND no
# denial does; denials only ever SUBTRACT, so the worst a wrong line here can do
# is lose a file -- the recoverable direction the allowlist already prefers.
#
# Every entry below was derived from an actual `pytest --collect-only` run inside
# a materialized export (120 collection errors), never from grepping for imports.
# That distinction is load-bearing: shipping a PARTIAL scripts/ci/ turned
# `No module named 'scripts'` into eight more specific errors, so a list carried
# over from the previous export would have been wrong in both directions.
#
# One error was deliberately NOT denied: tests/unit/ci/test_workflow_triggers.py
# needs .github/workflows/, which Workstream F ships. Denying it would delete a
# real gate from the public repo to make an intermediate tree green.
#
# SUPERSEDED 2026-08-29 (3e1c384ae) -- the decision above was right, but its
# stated consequence is no longer true, and the number below was measured before
# the fix. The file resolved .github/workflows at MODULE level and asserted the
# glob was non-empty, so the assert fired during IMPORT. A collection error is
# not a test failure: pytest raises Interrupted and discards the whole session.
# `pytest tests/` -- and `make test`, which is `pytest tests/unit/` -- collected
# NOTHING in the export, and the workaround was an --ignore flag a public user
# has no way to know they need.
#
# It now skips visibly when the directory is ABSENT (a scope decision) and still
# raises when the directory is PRESENT but empty (a defect). The gate survives in
# full for the tree that has .github/, which is what the decision above wanted.
#
# VERIFIED on a materialized export that was `git init`-ed first (the corpus
# helper needs a real checkout, so a bare directory reports six extra errors that
# are artefacts of the measurement, not defects):
#     2026-08-28, before the fix:  44,270 tests collected, 1 error  (Interrupted)
#     2026-08-29, after  the fix:  43,289 tests collected, 0 errors, no --ignore
# The count falls because denials landed in between and three modules now skip at
# module level rather than collecting; zero errors is the number that changed.
#
# WORKSTREAM F PREREQUISITE, verified by simulating the end state (copying
# .github/ in and deleting the two Claude workflows): the collection error
# clears, 40 pass, and ONE test then fails --
#     test_allowlist_has_no_stale_entries: "claude.yml: workflow no longer exists"
# _ALLOWED_NON_PR_EVENTS in that file still carries a claude.yml entry. F must
# delete that entry in the SAME commit that drops the workflow. It cannot be
# fixed earlier: on dev the workflow still exists, and removing its allowlist
# entry there makes a different test in the same file fail instead.
#
# That tension is structural, not a sequencing accident: the entry is REQUIRED in
# the tree where claude.yml exists and STALE in the tree where it does not, and a
# static dict cannot be right in both. Two resolutions, both cheap; F picks one.
#   (a) Elect one owner. test_allowlist_has_no_stale_entries treats an absent
#       workflow as stale UNLESS this allowlist denies it -- so "which workflows
#       ship" is answered here and nowhere else, and a workflow deleted without a
#       denial is still reported. Keeps the gate's full strength in both trees.
#   (b) Delete the entry as part of F's initial commit and accept that dev's copy
#       of the file diverges from the published one. Simpler, but it makes the
#       two trees' tests differ, which is the thing this allowlist exists to avoid.
# (a) is preferred. Whichever is chosen, plant the violation first: delete a
# non-Claude workflow WITHOUT a denial and confirm the test still goes red, or the
# fix has removed the gate rather than adapted it.
#
# The --ignore workaround this block used to require is GONE as of 3e1c384ae:
# the module skips visibly instead of aborting collection, so `pytest tests/`
# runs in the export unmodified. Do not reintroduce the flag -- it would hide the
# very regression the skip was added to make visible.
#
# Not globbed, on purpose. A glob would be safe by direction, but only a per-file
# entry can be reported stale by the dead-denial ratchet on the day its subject
# reappears or is renamed.

# scripts/sim2rank/ -- the metric meta-evaluation subsystem (65 files, 36,803
# LOC), a research programme rather than framework surface, and CLAUDE.md records
# it as under heavy active development. Its in-package half DOES ship
# (spectramr.core.metrics.meta_evaluation), so denying these costs real coverage:
# 16 of the 65 test FILES importing that subpackage. 49 survive, so it is a
# partial loss, not an untested subsystem -- measured, not assumed. Shipping the
# 36.8k LOC to recover 16 files is the worse trade.
#
# Re-measured 2026-09-05 (AST import scan of the private tree, checked against a
# fresh --strict export): 16/65/49. It read 15/63/48 when this block was written
# and drifted on corpus growth alone -- two new files, one denied, one surviving
# -- with no allowlist edit involved, so RE-MEASURE this line, never quote it:
#   python - <<'EOF'
#   import ast, pathlib
#   T="spectramr.core.metrics.meta_evaluation"
#   hit=lambda m,names: m==T or m.startswith(T+".") or (
#       m=="spectramr.core.metrics" and any(a.name=="meta_evaluation" for a in names))
#   f=[p for p in pathlib.Path("tests").rglob("*.py")
#      if any((isinstance(n,ast.ImportFrom) and hit(n.module or "", n.names))
#             or (isinstance(n,ast.Import) and any(a.name.startswith(T) for a in n.names))
#             for n in ast.walk(ast.parse(p.read_text())))]
#   print(len(f))   # then count how many exist in the export tree
#   EOF
!tests/contracts/test_metric_registry.py
!tests/unit/core/metrics/test_metric_directions.py
!tests/unit/core/metrics/test_metric_registry_health.py
!tests/unit/meta_evaluation/test_bradley_terry.py
!tests/unit/meta_evaluation/test_clinical_rankers.py
!tests/unit/meta_evaluation/test_gen2_tie_faithfulness.py
!tests/unit/meta_evaluation/test_information.py
!tests/unit/meta_evaluation/test_ms3_ranker.py
!tests/unit/meta_evaluation/test_sobol.py
!tests/unit/meta_evaluation/test_task_ranker.py
!tests/unit/test_acss_permutation.py
!tests/unit/test_sim2rank_anchor_calibration.py
!tests/unit/test_sim2rank_anchor_set.py
!tests/unit/test_sim2rank_axes.py
!tests/unit/test_sim2rank_axis_bank.py
!tests/unit/test_sim2rank_axis_subset_cli.py
!tests/unit/test_sim2rank_canonical_counts.py
!tests/unit/test_sim2rank_cd_diagram.py
!tests/unit/test_sim2rank_consensus_figures.py
!tests/unit/test_sim2rank_cpge_complex_context.py
!tests/unit/test_sim2rank_dataset_kind.py
!tests/unit/test_sim2rank_degradation_complex.py
!tests/unit/test_sim2rank_degradation_report.py
!tests/unit/test_sim2rank_degradation_seeding.py
!tests/unit/test_sim2rank_degradation_variable_size.py
!tests/unit/test_sim2rank_distributional_eval.py
!tests/unit/test_sim2rank_engine_outcomes.py
!tests/unit/test_sim2rank_engine_zero_metrics.py
!tests/unit/test_sim2rank_exclude_radiomics_provenance.py
!tests/unit/test_sim2rank_export_per_axis_tables.py
!tests/unit/test_sim2rank_figure_tree.py
!tests/unit/test_sim2rank_fourier_bridge.py
!tests/unit/test_sim2rank_fp32_numerics.py
!tests/unit/test_sim2rank_gen2_per_axis.py
!tests/unit/test_sim2rank_hib_labels.py
!tests/unit/test_sim2rank_identity_anchor_floor.py
!tests/unit/test_sim2rank_inter_metric_correlation.py
!tests/unit/test_sim2rank_interactive_report.py
!tests/unit/test_sim2rank_log_triage.py
!tests/unit/test_sim2rank_measured_noise.py
!tests/unit/test_sim2rank_measurement_noise.py
!tests/unit/test_sim2rank_meta_eval.py
!tests/unit/test_sim2rank_method_robustness.py
!tests/unit/test_sim2rank_metric_health.py
!tests/unit/test_sim2rank_mosaics.py
!tests/unit/test_sim2rank_odm_correctness.py
!tests/unit/test_sim2rank_odm_invariants.py
!tests/unit/test_sim2rank_per_axis_adr_aggregation.py
!tests/unit/test_sim2rank_per_axis_spearman.py
!tests/unit/test_sim2rank_probe_metrics.py
!tests/unit/test_sim2rank_ranking_invariants.py
!tests/unit/test_sim2rank_regime_gate.py
!tests/unit/test_sim2rank_region_source_dispatch.py
!tests/unit/test_sim2rank_region_sweep.py
!tests/unit/test_sim2rank_registry_parity.py
!tests/unit/test_sim2rank_response_clustering.py
!tests/unit/test_sim2rank_scanner_conditioning.py
!tests/unit/test_sim2rank_scoring_repairs.py
!tests/unit/test_sim2rank_severity_calibration.py
!tests/unit/test_sim2rank_simulator_calibration.py
!tests/unit/test_sim2rank_synthetic.py
!tests/unit/test_sim2rank_tidy_export.py
!tests/unit/test_sim2rank_transfer_badge.py
!tests/unit/test_sim2rank_unified_degradations.py
!tests/unit/test_sim2rank_xlsx_workbooks.py

# scripts/data/ -- dataset acquisition and manifest construction (63 files).
# Bound to on-disk cohort layouts that do not exist for a public user, and the
# fastMRI half cannot ship at all (DUA).
!tests/audit/test_inprogress_dataset_wiring_audit.py
!tests/unit/data/test_build_fastmri_brain_cohort_manifest.py
!tests/unit/data/test_extract_oracle_bssfp.py
!tests/unit/data/test_frame_contract.py
!tests/unit/data/test_gen_external_manifests.py
!tests/unit/scripts/test_build_cambridge_federated_manifest.py
!tests/unit/scripts/test_build_cross_site_manifest.py
!tests/unit/scripts/test_build_fastmri_plus_manifest.py
!tests/unit/scripts/test_build_mrixfields2026_manifest.py
!tests/unit/scripts/test_build_nist_mrf_manifest.py
!tests/unit/scripts/test_build_synthseg_region_cache.py
!tests/unit/scripts/test_dataset_file_types.py
!tests/unit/scripts/test_download_external_datasets.py
!tests/unit/scripts/test_export_h5_to_nifti.py
!tests/unit/scripts/test_extract_external_datasets.py
!tests/unit/scripts/test_fetch_fastmri_plus_annotations.py
!tests/unit/scripts/test_prepare_mrixfields_archive.py
!tests/unit/scripts/test_probe_dataset_layouts.py
!tests/unit/scripts/test_resolve_data_locations.py
!tests/unit/scripts/test_show_mrixfields_dimensions.py

# scripts/ci/ gates that are NOT in the shipped subset -- either corpus-dependent
# (the corpus is experiments/, which does not ship) or dropped for the reasons the
# gate table in docs/contributing/public_export.rst records one by one.
!tests/unit/ci/test_check_experiment_configs_load.py
!tests/unit/ci/test_check_witness_corpus.py
!tests/unit/ci/test_coverage_summary.py
!tests/unit/ci/test_cpu_test_array.py
!tests/unit/ci/test_docs_navigation_guard.py
!tests/unit/ci/test_select_impacted_tests.py
!tests/unit/infrastructure/physics/test_mask_golden_gate.py
!tests/unit/scripts/test_config_golden_gate.py
!tests/unit/scripts/test_migrate_model_block_keys.py
!tests/unit/scripts/test_migrator_flow_mapping_blindness.py
!tests/unit/scripts/test_verify_config_migration.py

# scripts/preprocessing/ (41 files) -- a standalone pipeline with its own package
# root, which these tests reach by sys.path.insert rather than by import.
!tests/unit/data/test_inspect_bart.py
!tests/unit/data/test_paired_manifest.py
!tests/unit/data/test_preprocess_ulf_paired.py
!tests/unit/preprocessing/test_discovery.py
!tests/unit/preprocessing/test_pipelines.py
!tests/unit/preprocessing/test_tasks.py
!tests/unit/preprocessing/test_types.py
!tests/unit/preprocessing/test_utils.py
!tests/unit/scripts/test_external_dataset_tooling.py

# One-off scripts under scripts/ that ship nowhere: migrations already applied,
# experiment-specific probes, cluster log parsers, and maintenance tools whose
# subject (.claude/skills/) is itself excluded.
!tests/unit/config/test_coil_processing_migration_parity.py
!tests/unit/diagnostics/test_coil_estimation_demo.py
!tests/unit/scripts/test_check_skill_health.py

# Same reason, one level up: this guard reads `git ls-files .agent
# .claude/skills`, and BOTH roots are denied above. In a real checkout that
# command exits 0 with no output (a missing pathspec is not an error to
# ls-files), so the test does not fail -- @pytest.mark.parametrize receives an
# empty list and the file reports green having graded nothing. Its own
# docstring names that failure mode for the untracked-scratch case; the export
# reproduces it for every case at once. A gate that cannot see its corpus is
# removed, not shipped to pass vacuously.
!tests/unit/test_rules_docs_use_current_import_prefix.py
!tests/unit/scripts/test_convert_synthseg_h5_to_pt.py
!tests/unit/scripts/test_exp11_energy_probe.py
!tests/unit/scripts/test_exp11_reverse_sampler_ab.py
!tests/unit/scripts/test_parse_dispatch_logs.py
!tests/unit/tools/test_cohort_forensics_review.py

# Tests reaching a module by sys.path.insert into a directory that does not ship
# (tools/refactoring/, scripts/...). The bare module name in the error is that
# insert, not a package -- checked one by one rather than assumed from the shape.
!tests/unit/ci/test_pytest_result_collector.py
!tests/unit/config/test_config_read_tracer.py
!tests/unit/scripts/test_reproduce_published_numbers.py
!tests/unit/test_feature_introspection.py
!tests/unit/tools/test_extract_classes.py

# scripts/benchmarking/ -- degradation-vector benchmarks, research surface.
!tests/unit/benchmarking/test_degradation_vectors.py

# scripts/training/*.sbatch -- the subject of each of these is denied above, so
# on a fresh clone they read_text() a file that does not exist. They also carry
# one site's SLURM identity as DATA (--account=johnsson, ${USER}@cougarnet.uh.edu)
# rather than as a detector input, which is why neutralising the literal (as
# tests/unit/infrastructure/execution/test_backends.py got) is not enough here:
# the test would then assert a placeholder against a script that never ships.
!tests/unit/scripts/test_dispatch_experiments.py
!tests/unit/scripts/test_submit_exp11_ema_warmup_ablation.py
!tests/unit/scripts/test_submit_exp11_fpk_ablation.py

# Corpus-dependent gates. experiments/ does not ship, so tracked_yamls() returns
# 0 and these lose the thing they exist to check. Measured on a git-init'd export
# (2026-08-28): 6 failed, 16 passed, 4 skipped. The empty corpus reaches them in
# three shapes, and only the third is silent --
#   loud   : an explicit anti-vacuity guard fires. "cohort directory is empty --
#            the guard would be vacuous"; "parsed to zero entries. If the debt was
#            genuinely drained, delete this test in the PR that drained it".
#   visible: @parametrize over an empty list -> pytest SKIPs with
#            "got empty parameter set" (4 of these).
#   SILENT : `for config_path in ALL_CONFIGS:` in the test BODY. The loop simply
#            does not execute and the test passes. test_config_validation.py
#            reports 7 green tests having validated nothing.
# Plan Workstream D: a gate that can no longer see its corpus is removed from the
# public lane, not left in to pass vacuously. Recorded in docs/known_limitations.rst.
!tests/unit/config/test_dead_legacy_key_spellings.py
!tests/unit/utils/test_config_load_baseline.py
!tests/smoke/test_config_validation.py
!tests/smoke/test_deep_config_integrity.py
!tests/smoke/test_vf_smoke.py
!tests/audit/test_experiment_yaml_syntax.py

# ---- runnable examples -----------------------------------------------------
examples/                  # E4: each of these must audit --probe clean before release

# ---- build / packaging -----------------------------------------------------
pyproject.toml
uv.lock
Makefile                   # D: targets pointing at pruned paths must go
codecov.yml
.pre-commit-config.yaml

# Sanitized by Workstream C: the one uncommented real path is now a relative
# default, section 9's prose no longer describes the retired hardcoded account
# (#1146), and the SIM2RANK_* block is omitted because its only readers live in
# scripts/sim2rank/, which does not ship -- advertising a knob whose reader is
# absent is the inert-knob over-claim this release exists to remove.
# Verified: 47 advertised variables, 0 with no reader in the exported tree.
.env.example
.gitattributes
.gitignore

# ---- licence, governance, citation ----------------------------------------
LICENSE                    # Apache-2.0
NOTICE
CITATION.cff               # personal identifiers belong HERE, and here only
.zenodo.json               # Zenodo deposition metadata; agreement with CITATION.cff
                           # is pinned by tests/unit/release/test_zenodo_deposit.py
MAINTAINERS.md             # ... and here
CODE_OF_CONDUCT.md
CONTRIBUTING.md
GOVERNANCE.md
SECURITY.md
DISCLAIMER.md              # not-for-clinical-use; the CLI warning points at it
README.md
CHANGELOG.md               # de-linked 2026-09-03, and it carries no bare #N at all.
                           #    Not for the reason first assumed. MEASURED 2026-09-03: a
                           #    blob-rendered .md does NOT autolink #N. psf/black's
                           #    CHANGES.md carries 826 bare refs and 0 explicit links,
                           #    and GitHub renders all 826 as plain text -- its single
                           #    anchor comes from a full URL (line 1027), which GFM
                           #    autolinks and then displays as '#3968'. Autolinking is a
                           #    CONVERSATION feature: POST /markdown mode=gfm with
                           #    context=<repo> rewrote '#1' into that repo's PR #1 and
                           #    left '#9' and '#341' alone -- it fires only for numbers
                           #    that EXIST, so the hazard grows as the repo accrues PRs,
                           #    and it reaches these files only when their text is quoted
                           #    into a PR/issue body or commit message.
                           #    The reason that does hold is citation: the export drops
                           #    CLAUDE.md, .agent/rules/ and .claude/skills/ (all three
                           #    verified absent from the shipped tree), so 'pitfall #9'
                           #    points a reader at nothing while the '#' makes it look
                           #    like a clickable repository reference. All 33 refs across
                           #    the 7 .md files that had them now read 'pitfall 15' /
                           #    'internal issue 341'; the 5 survivors are URL anchors
                           #    inside link targets. Re-check with:
                           #      grep -rhoE '#[0-9]+' --include='*.md' <export>

# ---- example arms ----------------------------------------------------------
experiments/templates/     # the copy-me templates (see #1529 before shipping these)
#
# ...but only the one that loads. A template is copied, so a template that does
# not parse is worse than an absent one: the reader's first act is to inherit the
# defect. Both denials measured with `spectramr audit` on this branch:
#
#   categorical_values_reference.yaml       exit 2 -- "config_version is required
#                                           ... Must be one of ['1.0']"
#   comprehensive_config_template_v5.0.yaml exit 2 -- "Config version 5.0 not
#                                           supported. Accepted values: ['1.0']"
#
# The first is rotted and hand-maintained with no generator, superseded by the
# machine-checked v1.0_reference.yaml; the second is deferred-v5 backlog whose
# tracking doc does not ship either.
#
# The survivor, comprehensive_config_template.yaml, LOADS -- its one remaining
# audit failure is a health check, not a parse error, and PR #1568 fixes it.
# Pinned by tests/unit/scripts/test_export_public_tree.py::
# test_every_experiment_yaml_that_ships_actually_loads, so deleting a denial
# without fixing the file it denies goes red.
!experiments/templates/categorical_values_reference.yaml
!experiments/templates/comprehensive_config_template_v5.0.yaml

# Three exemplar arms, one per paradigm family, so the shipped tree carries a
# config that can actually be run rather than only a template to fill in. All
# three are at config_version 1.0 with a declared workflow regime x task, and
# all three read M4Raw (CC-BY-4.0) -- no fastMRI-derived data, which the DUA
# forbids republishing. They name the two manifests the builder below writes.
experiments/inprogress/workflow_baselines/b1_structural_recon_m4raw.yaml
experiments/inprogress/reconstruction/ssdu_selfsup_m4raw.yaml
experiments/inprogress/diffusion/experiment_96_sde_diffusion.yaml

# ---- tests whose SUBJECT does not ship -------------------------------------
# Found by walking every shipped test for a `Path(__file__).parents[N] / ...`
# construction and asking the exported filesystem whether the result exists.
# Every file below has 100% of its tests failing in the export -- measured, not
# predicted: 330 failed + 246 errors across the wider casualty set.
#
# Only files where EVERY test fails are denied here. Files that merely contain
# some corpus-dependent tests keep their surviving unit tests and get a guard
# instead; denying those would discard working coverage to remove a few cases.
#
# Per-file, not globbed, for the same reason as the block above: the dead-denial
# ratchet can then report the entry stale on the day its subject ships.

# scripts/sim2rank/ drivers -- extends the sim2rank decision already taken
# above, which denied the library-side tests but missed these eight; they
# reference the driver scripts rather than the in-package half.
!tests/unit/test_sim2rank_brain_sbatch.py
!tests/unit/test_sim2rank_build_report.py
!tests/unit/test_sim2rank_degradation_snapshots.py
!tests/unit/test_sim2rank_figure_dedup.py
!tests/unit/test_sim2rank_figure_legend_layout.py
!tests/unit/test_sim2rank_figure_manifest.py
!tests/unit/test_sim2rank_figure_style_gallery.py
!tests/unit/test_sim2rank_style.py

# scripts/diagnostics/ -- forensic tooling for cluster runs, not framework surface
!tests/unit/diagnostics/test_coil_map_aliasing.py
!tests/unit/diagnostics/test_kspace_phase_collapse.py
!tests/unit/diagnostics/test_render_report_cases.py
!tests/unit/diagnostics/test_trace_exp11_dataflow.py
!tests/unit/scripts/test_vf_mechanism_demo.py

# scripts/ci/, scripts/audit/, scripts/migrations/, scratch/ -- internal gates,
# corpus censuses and one-shot migrations. The migrations in particular operate
# on experiments/inprogress/, which does not ship at all.
!tests/unit/audit/test_framework_coverage_audit.py
!tests/unit/ci/test_a6_batch_key_census.py
!tests/unit/ci/test_a6_triage_orphan_keys.py
!tests/unit/scratch/test_compile_diagnostics.py
!tests/unit/scripts/test_check_produced_by_arm_resolves.py
!tests/unit/scripts/test_cluster_verify.py
!tests/unit/scripts/test_migrate_config_version_to_v1.py
!tests/unit/scripts/test_migrate_metrics_to_compute_list.py
!tests/unit/scripts/test_report_discarded_config_keys.py

# scripts/data/ -- dataset indexing helpers run on the cluster
!tests/unit/data/test_inspect_m4raw_quality.py
!tests/unit/data/test_mrixfields_proxy_manifest.py

# experiments/inprogress/ cohort audits -- these assert invariants OVER the 647
# arms, which the experiments/ decision removes wholesale. Nothing survives the
# corpus being absent, so there is no guard to add.
!tests/unit/config/test_exp11_phase5_ablation_deconfounded.py
!tests/unit/config/test_inprogress_audit_fixes_2026_06_19.py
!tests/unit/config/test_inprogress_baseline_resolves.py
!tests/unit/config/test_kspace_filling_cohort_drained.py
!tests/unit/config/test_mrixfields_ema_generative_arms_2026_07_04.py
!tests/unit/config/test_pnp_arm_wired.py

# ---- second census pass: subjects reached through a module-level alias -------
# The first block above was found by an AST scan that only matched an INLINE
# `Path(__file__).resolve().parents[N] / "a" / "b"` chain. That scan was blind to
# the two-statement form
#     REPO_ROOT = Path(__file__).resolve().parents[3]
#     SCRIPT    = REPO_ROOT / "scripts" / "ci" / "x.sh"
# which is how tests/unit/ci/test_refresh_diagnostics_script.py sat in the export
# failing 12/14 without appearing in the census at all. Re-running alias-aware
# raised the count from 52 files to 91. Same rule as above: only files where
# EVERY test fails are denied; partial files keep their coverage and get a guard.

# .github/ + TODO/ -- unlike test_workflow_triggers.py (deliberately kept, since
# Workstream F's .github/ alone repairs it), this one ALSO needs
# TODO/production_plan/, which never ships. F cannot repair it.
!tests/architecture/test_required_lane_composition.py
# ...and its plant harness with it. A detector and the violations planted against
# it are one unit: the harness reads the detector's SOURCE off disk
# (`_DETECTOR = _REPO_ROOT / "tests" / "architecture" /
# "test_required_lane_composition.py"`, line 37), so shipping the harness without
# the detector is 15 collected tests and 15 FileNotFoundError failures -- measured
# in the export, all 15 of the file's tests, which is the 100%-failing bar this
# allowlist uses for denial rather than a guard. Denying only the detector is the
# easy half to notice; there is no census that pairs them, because the harness
# names its subject as a path to another TEST file rather than to src/ or to a
# corpus root.
!tests/unit/architecture/test_required_lane_composition_plants.py

# docs/lean/ (Lean 4 proof layer), in the explicitly-dropped docs subset.
#
# `!tests/unit/docs/test_framework_paper.py` used to sit here beside these four.
# `3f4032bd7` ("docs: cull to usage-only") deleted 2,067 doc files including
# `docs/framework_paper.md` AND that test, so the denial became a dead pattern
# and `--strict` began exiting 2 on it. A dead denial is deleted, not kept: it
# reads as a decision that is still holding something back when it is holding
# back nothing, and the next file to land on that path would ship unremarked.
!tests/unit/docs/test_sim2rank_betting_layer.py
!tests/unit/docs/test_sim2rank_conformal_layer.py
!tests/unit/docs/test_sim2rank_kemeny_layer.py
!tests/unit/docs/test_sim2rank_transfer_layer.py

# scripts/ci/ gates and their corpus baselines. Workstream D's rule: a gate that
# can no longer see its corpus leaves the public lane rather than passing vacuously.
!tests/unit/ci/test_check_acceleration_ladder_realisable.py
!tests/unit/ci/test_check_getattr_names_a_real_field.py
!tests/unit/ci/test_run_required_locally.py                 # scripts/ci/run_required_locally.py; 0 / 14
!tests/unit/ci/test_rerun_wrapper.py
!tests/unit/ci/test_smoke_wrapper_hygiene.py
!tests/unit/scripts/test_check_experiment_configs_load.py
!tests/unit/scripts/test_check_model_kwargs_are_read.py
!tests/unit/scripts/test_check_ulf_operator_is_wired.py     # scripts/ci/check_ulf_operator_is_wired.py; 0 / 16
!tests/unit/scripts/test_smoke_wrapper.py

# scripts/sim2rank/ -- third and final group; same decision as the two above.
!tests/unit/scripts/test_env_knob_advertisement.py
!tests/unit/test_sim2rank_algorithm_axis_normalization.py
!tests/unit/test_sim2rank_fp32_default.py
!tests/unit/test_sim2rank_manifest_integration.py
!tests/unit/test_sim2rank_mode_guard.py
!tests/unit/test_sim2rank_per_axis_all_generations.py
!tests/unit/test_sim2rank_seed_flag.py                      # 0 / 8
!tests/unit/test_sim2rank_segment_folder.py

# Validation-image / mosaic tooling (scripts/*.py, scripts/ci/audit_validation_images.py).
# Considered shipping the five scripts instead to recover 98 tests -- rejected on
# measurement, not taste: all five themselves read experiments/, TODO/ or
# tests_experiments/, so shipping them relocates this same problem rather than
# solving it.
# `tests/unit/tools/test_audit_validation_images.py` was denied here from the
# start; `tests/unit/scripts/test_audit_validation_images.py` -- a SECOND test
# file for the SAME script -- was not. Both load
# scripts/ci/audit_validation_images.py by path. The census that built this block
# grouped by the test's own directory, so the pair was never seen as a pair.
!tests/unit/scripts/test_audit_validation_images.py         # 0 / 2 (+1 cohort-absent skip)
!tests/unit/scripts/test_validation_mosaic_audit.py
!tests/unit/tools/test_audit_validation_images.py
!tests/unit/tools/test_generate_validation_mosaic.py
!tests/unit/tools/test_mosaic_since_filter.py
!tests/unit/tools/test_mosaic_validation_cli.py
!tests/unit/tools/test_process_smoke_log.py
!tests/unit/tools/test_validation_image_dirs.py

# scripts/{eval,evaluation,common,training}/ -- cluster drivers and sbatch wrappers.
!tests/unit/scripts/test_evaluate_mrixfields_baselines_cli.py
!tests/unit/scripts/test_run_test_inference.py
!tests/unit/scripts/test_torch_ld_path.py

# tools/docs/generate_key_reference.py -- the generator for
# docs/config_key_reference.rst. NOTE for Workstream G: if that generated page
# ships, it ships without its generator, so it can only be regenerated on dev.
!tests/unit/config/test_key_reference_is_current.py

# experiments/{inprogress,validated,campaigns,ablation,active,training}/ corpora.
# test_no_cluster_paths.py is itself a sanitization guard, but it scans the
# experiments corpus -- with no corpus it has nothing to guard, and it fails
# loudly rather than passing empty.
!tests/audit/test_vf_scientific_validity_fixes_2026_06.py
!tests/unit/config/test_gan_arms_have_discriminator_component.py
!tests/unit/config/test_validation_image_saving_config.py
!tests/unit/infrastructure/training/strategies/test_vae_kl_weight_wiring.py
!tests/unit/orchestration/test_frontier_benchmark_manifest.py
!tests/unit/pipelines/test_trust_ablation_spec.py
!tests/unit/test_no_cluster_paths.py

# ---- deny block 3: tests that IMPORT a module the export drops --------------
# A third census shape. The two before it scanned *path construction*; these
# reach an unshipped subject through the import system instead:
#
#     from scripts.sim2rank.scoring import compute_adr
#         -> ModuleNotFoundError: No module named 'scripts.sim2rank'
#
# Nearly every one is FUNCTION-LOCAL, so a line-anchored grep finds none of them
# -- the same blindness CLAUDE.md records for check_layering.sh, reached by a
# different route. The scan is AST-based (ast.walk), which is what makes nesting
# irrelevant.
#
# Denied only where the file is 100% failing, measured in the export tree. The
# eight partial ones keep their surviving tests and need per-test guards (#1589);
# test_sim2rank_native_data_wiring.py needs nothing -- its import already sits
# behind a data-availability skip and reports 2 skipped.
#
# Why scripts/sim2rank/ is not shipped instead, which would recover all of these:
# the in-package half ALREADY ships and stands on its own -- 45 modules under
# core/metrics/meta_evaluation/ with 89 tests passing in the export -- so this is
# not the "documented feature that is inert" case. Shipping the drivers means
# +36,775 LOC of research tooling and re-opening sanitization on five files that
# carry cluster identity (4 .sbatch + task_networks/nnunet_segmentation.py).
# Reversible: delete these eight lines and add scripts/sim2rank/ to ship it.
# (Was seven when this block was written; test_sim2rank_bt_score_vectorization.py
# arrived on dev at 07ed477e3, after the census, and is the same 100%-failing
# shape -- its import sits at module level, so all 8 tests die at collection.)
!tests/unit/data/test_marker_artefacts.py                   # scripts.data.generate_marker_artefacts; 0 passed / 2 failed
!tests/unit/test_sim2rank_adversarial_review.py             # 0 / 6
!tests/unit/test_sim2rank_bt_score_vectorization.py          # 0 / 8
!tests/unit/test_sim2rank_followups.py                      # 0 / 11
!tests/unit/test_sim2rank_radiomics_filter.py               # 0 / 18
!tests/unit/test_sim2rank_tier2_rankers.py                  # 0 / 20
!tests/unit/test_sim2rank_v2_additions.py                   # 0 / 11
!tests/unit/test_sim2rank_v3_additions.py                   # 0 / 10

# Added to dev AFTER the tree was published, and measured in a materialized export
# (2026-08-30) rather than inferred from their imports:
#   test_production_plan_batch_partition.py plants against
#     TODO/production_plan/tools/check_batch_partition.py, and TODO/ is a dropped
#     root -- ModuleNotFoundError at IMPORT, so pytest raises Interrupted and the
#     export's whole session collects NOTHING. The severe shape: not one lost test.
#   test_rename_package_identifier.py plants against
#     scripts/migrations/rename_package_identifier.py, which does not ship: 14 errors.
# Both are correct on this branch; neither has a subject in the distribution.
!tests/unit/tools/test_production_plan_batch_partition.py    # collection error -> Interrupted
!tests/unit/scripts/test_rename_package_identifier.py        # 0 / 14

# ---- deny block 4: measured by failure-set diff, not by census -------------
# The three blocks above scan for a SHAPE (an inline path chain, a module-level
# alias, an AST import) and then measure what they found. This one inverts it:
# run the suite in both trees at ONE sha and diff the failure sets, so a file is
# reached by its behaviour rather than by a pattern someone thought to look for.
# That is what caught the pair below that the directory-grouped census split, and
# the four whose subject is named only inside a fixture.
#
# Why a diff and not a count: `dev` is standing-red, so "N failed in the export"
# proves nothing about the boundary. Method -- export_public_tree.py --sha
# 1795484c0 --strict into a fresh dir, `git worktree --detach` the same sha for
# the control, one `pytest --junitxml` session each, same .venv, same machine.
#
# `git init` in the export is load-bearing: tests/utils/corpus.py::repo_root()
# emits a visible skip when no .git sits above it, so a plain exported directory
# SKIPS the corpus tests while the real public repo -- a checkout -- FAILS them.
# Measuring in a non-git export understates this by the whole corpus.
#
# Measured (2026-09-05, 64 candidate files): 975 export testcases against 4292
# private, 915 in common. 272 fail ONLY in the export; 17 fail in both (standing
# `dev` debt, out of scope here); 4 fail only in private. The 60 cases unique to
# the export are all SKIPS, so nothing hid in the gap.
#
# Those 272 split by the bar this file already uses twice: 20 files / 134
# failures are 100% failing and denied below; 35 files / 138 failures keep
# passing tests and get per-test guards instead (#1589). Every skip counted
# against a denied file below is pytest's "got empty parameter set" -- the
# @parametrize-over-an-empty-corpus shape, which is absence of coverage, not
# coverage. No denied file has a single passing test in the export.
#
# experiments/{inprogress,campaigns}/ -- 14 files whose subject is one arm, or a
# cohort, read by path. Nothing survives the corpus being absent.
!tests/audit/test_inprogress_knee_cohort_invariants.py       # 0 / 2
!tests/audit/test_kspace_filling_config_honesty.py           # 0 / 1
!tests/unit/builders/test_optimization_builder_ttur_2026_06.py  # 0 / 3
!tests/unit/config/test_exp11_kspace_filling_loss_weights.py  # 0 / 1
!tests/unit/config/test_f11_singleton_yaml_fixes.py          # 0 / 4
!tests/unit/config/test_f16_generator_build_yaml_fixes.py    # 0 / 8
!tests/unit/config/test_mrixfields_validation_batch_cap_2026_07_19.py  # 0 / 3
!tests/unit/config/test_vf_arms_fully_load.py                # 0 / 1
!tests/unit/experiments/test_contrast_field_agnostic_arms_resolve.py   # 0 / 6
!tests/unit/experiments/test_direct_ulf_to_hf_sr.py          # 0 / 6
!tests/unit/experiments/test_mrixfields_arms_audit.py        # 0 / 2
!tests/unit/experiments/test_raster_control_single_knob.py   # 0 / 5
!tests/unit/reporting/test_mrixfields_reporting_coverage.py  # 0 / 1
!tests/unit/test_vf_digital_twin_wiring.py                   # 0 / 49 -- the largest single file
#
# scripts/eda/ -- the dataset-EDA runner, a cluster-side exploratory tool.
!tests/unit/data/eda/test_cli.py                             # scripts/eda/dataset_eda_runner.py; 0 / 1
#
# scripts/container/ -- Dockerfile + spectramr.def, neither of which ships. Its
# two other tests already "pass" as `if not path.exists(): pytest.skip` -- the
# absence-keyed shape this file rejects, since it cannot tell the publication
# boundary from someone deleting the Dockerfile. Denying the file removes the
# blind guard with it rather than leaving it in the public tree as a green.
!tests/unit/scripts/test_container_entrypoint.py             # 0 / 1 (+2 absence-skips)

# ---- CI workflows ----------------------------------------------------------
# Workstream F's condition is met: the two Claude workflows are denied below, so
# no CLAUDE_CODE_OAUTH_TOKEN is implied by a public checkout (and no such secret
# exists there anyway).
#
# They do NOT ship inert. Actions is ENABLED on adnaneGdihi/spectramr -- measured on
# the remote, and the three merged dependabot PRs there are checks that actually ran.
# The earlier note here said the opposite and pinned that claim in
# docs/known_limitations.rst; it was true when the repo was created and stopped being
# true without one character of this file changing. Treat the lane as live: it fires
# on real PRs, so a workflow shipped here must be one the public tree can actually run.
# They ship because tests/unit/ci/test_workflow_triggers.py takes them as its subject,
# and `make gate` DERIVES the local lane from pr-required.yml rather than restating it.
#
# pull_request_target / issue_comment / issues workflows are read from the DEFAULT
# branch, so these must be present on the published `main` or they stay inert there.
.github/

# The two Claude workflows. `claude.yml` is issues/issue_comment-triggered and
# `claude-code-review.yml` runs on every PR; both consume
# secrets.CLAUDE_CODE_OAUTH_TOKEN. A comment-triggered workflow holding a long-lived
# OAuth token on a PUBLIC repo is an exposure path in a way it is not on a private
# one, and the secret does not exist in the published repo regardless -- so the first
# push would go red even if the exposure were acceptable. Measured: these are the only
# two workflows referencing any secret beyond the auto-provided GITHUB_TOKEN.
!.github/workflows/claude.yml
!.github/workflows/claude-code-review.yml

# manual-full-suite.yml was DENIED here until 2026-09-04, for a reason this change
# inverts rather than overrules. The reason was sound: the private file drives the
# experiment corpus and two scripts that do not ship
# (scripts/ci/check_performance.py, scripts/ci/submit_cpu_test_array.sh), and it is
# workflow_dispatch-only -- so it would never fail on its own and would sit in the
# Actions tab looking like a capability the repo has. That is the advertised-but-inert
# shape (non-negotiable 16) with a green-looking surface, which is worse than an
# absent workflow.
#
# What changed is that the public tree now needs a maintainer lane, and the two
# properties that made the private file inert are both fixed by the overlay copy at
# scripts/release/public_overlay/.github/workflows/manual-full-suite.yml:
#
#   * it carries a weekly `schedule:` cron, so it fails on its own. A cron is read
#     from the DEFAULT BRANCH; on the public repo `main` is both the default branch
#     and what the export writes, so it fires there and would not here.
#   * the `performance` job -- the half that needed the two unshipped scripts -- is
#     absent from the overlay rather than carried and skipped.
#
# The denial had to go, and not for a bookkeeping reason: read_overlay is
# REPLACE-ONLY. It rewrites the content of a path the allowlist already selects and
# reports an overlay on any other path as a DEAD OVERLAY, exit 2. So an overlay on a
# denied path is written nowhere. Deleting the denial and adding that overlay file
# are one decision; neither works alone.
#
# Note there is NO positive line for this path, deliberately. `.github/` above
# already selects everything under it, so an explicit allowance here matches nothing
# the directory allowance had not already matched and the exporter reports it as a
# DEAD PATTERN (exit 2) -- which is how this was caught rather than reasoned out.
# Removing a denial IS the allowance when a parent directory ships.

# tests/unit/ci/test_workflow_triggers.py READS the two denials above -- it parses
# this file's `!.github/workflows/` lines with the same comment rule as
# parse_allowlist, so a denial added here needs no companion edit there. That link
# matters because _ALLOWED_NON_PR_EVENTS carries a `claude.yml` key whose workflow is
# absent in the export; without it the export would ship a test red on arrival.
#
# The test keeps a second register, _NOT_DISTRIBUTED, mapping each such workflow to
# WHY it is not distributed. That is not a competing copy of this list: membership is
# decided here and only here, and an excuse this file no longer backs is itself a
# reported failure ("the excuse names a scope decision that public_allowlist.txt no
# longer makes"). So the direction is fixed -- every _NOT_DISTRIBUTED name must appear
# here, and a denial added here needs an excuse only if the workflow also holds an
# _ALLOWED_NON_PR_EVENTS key. Verified on the merged tree, not inferred: neither
# manual-full-suite.yml nor claude-code-review.yml holds one, so the denial above is
# self-contained.

# ---- submodule pointers ----------------------------------------------------
.gitmodules                # external/ baselines are pointers, not vendored code
external/

# ---- docs: the curated public site (Workstream G) --------------------------
# Still deliberately NOT "docs/": the tree holds 2,332 files and the exclusion note
# that used to sit here measured ~68% of them citing pre-refactor src/<x>/ paths.
# The curated set below was measured on its own terms rather than inheriting that
# figure -- 35 src/<x>/ citations across 9 of its 58 content pages -- and
# scripts/ci/check_docs_paths_exist.py (shipped below) is the gate that keeps a
# published page from naming a path the published tree does not have.
#
# This list reproduces what adnaneGdihi/spectramr already carries. It was hand-
# assembled there, so until now a re-export would have DELETED 57 of these pages.
# docs/index.rst ships FROM THIS BRANCH, not from the overlay. The public toctree may
# name only pages that ship (Read the Docs builds with -W), and there were two ways to
# get that: overlay a public index over an internal one, or make the tracked index the
# public one and move the internal-only entries to a separate `:orphan:` page. The
# second wins -- see docs/index_internal.rst, which is not allowlisted below and so is
# excluded by construction. The overlay form kept two hand-maintained lists of the same
# 38 pages with nothing comparing them; the split form is checked by
# check_docs_navigation.py, which is what reported the 38 duplicate toctree entries when
# the orphan page was briefly made comprehensive. One audience, one list, one owner.
docs/tutorials/
docs/how_to/
docs/explanation/
docs/reference/
docs/modules/
docs/_static/
docs/conf.py
docs/Makefile
docs/index.rst                      # the PUBLIC index; internal-only entries live
#                                     in docs/index_internal.rst, which does not ship
#
# Getting started / using it
docs/getting_started.rst
docs/user_guide.rst
docs/troubleshooting.rst
docs/known_limitations.rst          # E6: what the shipped tree does NOT do, stated
docs/versioning.rst                 # what MAJOR.MINOR.BUILD means, which branch carries
#                                     which number, and why a nightly is `.devN` and not
#                                     `+build` (PyPI rejects local versions outright)
#
# Driving it from configuration
docs/config_schema_reference.rst
docs/config_key_reference.rst       # generated; its generator ships below
docs/transform_registry.rst
docs/environment_variables.rst
docs/CLUSTER_DATA_LAYOUT.md
#
# Running it
docs/running_pipelines.rst
docs/execution_modes.rst
docs/cli_reference.rst
docs/plugins.rst
docs/campaigns_user_guide.rst
docs/hpo_guide.md
docs/audit_ladder_user_guide.rst
docs/accelerated_run_contract.rst
docs/distributed_training.rst
docs/training_throughput.rst
#
# Results, logging, debugging
docs/run_provenance_and_logging.rst
docs/reporting.md
docs/reporting_pipeline.rst
docs/debug_snapshot_contract.rst
#
# What you can configure
docs/models_reference.rst
docs/model_capabilities.rst
docs/model_registry_reference.rst
docs/MODEL_TASK_MAPPING.md
docs/strategies_reference.rst
docs/losses_reference.rst
docs/metrics_reference.rst
#
# API + theory
docs/scripting_api.rst
docs/sim2rank_reliability_theory.rst
#
# Read the Docs reads its config from the REPO ROOT, which is why the file moved
# there from docs/ in this change rather than being configured with a custom path.
.readthedocs.yaml

# ---- conda packaging -------------------------------------------------------
# The recipe reads its version out of src/spectramr/__init__.py with
# load_file_regex, so it adds no fifth version declaration for build_dist.py to
# reconcile. It is parse-verified only: `conda build` has never been run on it (no
# conda on the authoring host), so the first real build IS its first test.
conda/

# ---- scripts: CURATED SUBSET, pending Workstream C -------------------------
# Deliberately NOT "scripts/". 414 files, many carrying cluster paths, SLURM
# account names and institution identifiers that Workstream C must sanitize first.
scripts/release/
#
# ...minus the public-repo settings gate. This family is the export's POLICY,
# not its product: nothing in the published tree consumes them, and they state
# in one place exactly which protections are and are not enforced on the public
# remote, next to the maintainer login that reviews its deployments.
#
# They also cannot run there. Asserting rulesets and environment protection
# needs repository-admin scope, which the workflow GITHUB_TOKEN does not have --
# so a public "settings drift" job would require storing an admin PAT as a
# secret on a public repository. That trades a settings-drift check for a much
# worse exposure, which is why this gate is a publish PRE-FLIGHT run from the
# private tree (CLAUDE.md, "Publishing a new snapshot" step 0) rather than a
# public CI job.
#
# Named one by one, NOT as `scripts/release/public_settings_*.py`: the exporter
# fnmatches and would honour a glob, but `_denied_file_paths()` in
# tests/unit/scripts/test_export_public_tree.py skips any pattern containing a
# `*`, so a globbed denial is invisible to the gate that checks shipped tests do
# not reach denied paths. Concrete lines trade a convenience for a live check.
#
# Concrete lines fail OPEN when the family grows -- a new sibling module ships
# unless someone remembers a line here, and a forgotten denial publishes a file
# invisibly (NN21). `test_every_settings_module_is_denied_from_the_export` in
# tests/unit/release/test_public_settings_apply.py closes that: it globs the
# family off disk and goes red on any member without a denial.
!scripts/release/public_repo_settings.yaml
!scripts/release/assert_public_repo_settings.py
!scripts/release/public_settings_diff.py
!scripts/release/public_settings_apply.py
!scripts/release/public_settings_identity.py
!scripts/release/public_settings_model.py
!scripts/release/public_settings_negatives.py
# The test goes with its subject. A shipped test whose subject is denied fails
# outright in the export -- there are already ~46 such failures, and each one
# costs a real finding its visibility by sitting in the noise.
!tests/unit/release/test_public_settings_diff.py
!tests/unit/release/test_public_settings_apply.py
# The loader those two share is deliberately NOT denied. It is also imported by
# tests/unit/release/test_bump_version.py, whose subject
# scripts/release/bump_version.py ships -- and a dropped conftest.py is a
# strictly worse failure than a dropped subject: each of the ~46 above fails one
# test, whereas a missing conftest.py aborts collection for the whole directory
# and takes test_zenodo_deposit.py down with it. Worse still, pytest then
# resolves the bare name against the ROOT conftest.py, so the error reads
# "cannot import name load_release_module" rather than "no such module".
# Shipping it leaks nothing: it is a path-loader for scripts/release/*.py and
# holds no repo-settings detail. Pinned by
# test_no_shipped_test_imports_a_dropped_conftest -- do not re-add the denial
# without first denying every test that imports it.
#
# The CI gates, named one by one rather than as "scripts/ci/". Each one below was
# RUN inside a materialized export and reported a non-vacuous result; the gates
# left out are dropped because they print OK over a corpus that is not there.
# Their baselines are the reason the directory cannot ship wholesale:
# witness_baseline.txt alone names 487 internal experiment arms.
scripts/ci/check_layering.sh              # PASS, self-reports non-vacuous, 12 baselined
scripts/ci/layering_baseline.txt
scripts/ci/check_dataloader_construction_ssot.py   # OK, 5 allow-listed sites found
scripts/ci/dataloader_binding_names.txt
scripts/ci/refresh_dataloader_binding_names.py     # regenerates the file above
scripts/ci/check_health_check_names.py    # OK, 147 methods emitting 158 names
scripts/ci/health_check_names.txt
scripts/ci/check_f821_ratchet.py          # non-negotiable 19's ratchet
scripts/ci/baselines/f821.txt             # 5 of its 26 rows name files that do NOT
scripts/ci/baselines/f821_unparseable.txt #   ship, so the gate exits 1 in the export.
#   It CANNOT be re-derived here: dropping those rows un-exempts names that still
#   exist on dev, which would redden dev's own gate. F regenerates it once in the
#   new repo (`--write`). The direction is DOWNWARD -- rows removed, never a raised
#   ceiling -- which is the only direction non-negotiable 20 permits.
scripts/ci/check_test_paired_with_source.py
scripts/ci/check_no_stale_package_name.py # the third .pre-commit-config.yaml local
#   hook. It is a RUNNER, not a rule: NEEDLES and _offending_files come from
#   tests/architecture/test_no_stale_package_name.py, which ships under the wholesale
#   tests/ allowance. Shipping the config while dropping this file leaves the published
#   repo with a hook whose entry does not exist -- `pre-commit run --all-files` in
#   pr-advisory then fails with "No such file or directory", the red X that lane's
#   header exists to prevent -- and strands the shipped
#   tests/unit/scripts/test_check_no_stale_package_name.py on an absent subject, the
#   same shape as the migrate_config_keys pair below. Caught by verifying a
#   materialized export, not by reading this list; the detector that now catches it
#   automatically is test_every_shipped_pre_commit_hook_entry_ships_its_subject.
scripts/ci/lint_changed_lines.py
# The two docs gates the published pr-required.yml already invokes. They were
# missing from this list while the published tree carried them, so the export and
# the publication disagreed in both directions at once. check_docs_paths_exist.py
# is corpus-independent BY DESIGN -- it takes the tree as an argument precisely so
# it can be run against the export.
scripts/ci/check_docs_navigation.py
scripts/ci/check_docs_paths_exist.py
# The RENAMES fixer and its paired check. Both take their roots as arguments
# (DEFAULT_ROOTS is only a default), so they run against a reader's OWN configs
# rather than needing this tree's corpus. The published repo already carried
# tests/unit/scripts/test_check_no_legacy_config_keys.py while shipping neither
# script -- a test whose subject is absent, which is the exact shape the denial
# block above exists to prevent. Shipping the pair fixes it in the direction that
# adds a capability rather than deleting a test.
scripts/ci/migrate_config_keys.py
scripts/ci/check_no_legacy_config_keys.py
scripts/ci/report_discarded_config_keys.py  # the detector that answers "is this key
#   actually read?" -- 155 lines, stdlib plus spectramr.config.settings and
#   spectramr.core.execution_ledger, both of which ship. It ships because CONTRIBUTING
#   tells a contributor to run it and because the shipped
#   tests/unit/scripts/test_export_public_tree.py loads it as the single owner of what
#   "discarded" means; without it that test fails in the export rather than skipping.
# The witness gate. Its subject EXISTS here now -- experiments/inprogress ships the
# three exemplar arms -- so it runs for real rather than over an absent corpus, and
# .pre-commit-config.yaml invokes it on every commit. Deliberately WITHOUT
# scripts/ci/witness_baseline.txt: that file names 487 internal arms, and an absent
# baseline makes read_baseline() return the empty set, so the public gate accepts no
# inherited debt. Strict by construction is the right posture for a fresh tree.
scripts/ci/check_witness_corpus.py

# ---- the one file out of tools/ --------------------------------------------
# docs/config_key_reference.rst declares itself generated by this script and tells
# the reader to run it. Shipping the page without it publishes an instruction that
# cannot be followed -- the same bar `make help` was judged against: an inert
# feature is acceptable unadvertised, not acceptable documented as working.
# Only this file, not tools/: relocating the directory lands two new over-ceiling
# entries in large_files.txt (#1492), so it is a decompose-on-move job.
tools/docs/generate_key_reference.py
#
# Makefile targets call these; all four are stdlib-only and corpus-independent.
scripts/coverage/print_per_layer.py
scripts/coverage/missing_test_files.py
scripts/maintenance/prove_reachable.py
scripts/verify/verify_dependencies.py

# The only shipped way to build the v3 JSON manifests the exemplar arms name in
# data.index_path. Without it those arms are resolvable but not runnable, and
# nothing says so -- `audit --probe` is a synthetic forward probe that never
# constructs the dataset, so an arm passes 150 checks while naming an index file
# that exists nowhere.
#
# ORDERING: this line is only safe once #1552 is on dev. Until then the
# exported copy still carries a /project/<pi>/<user>/ fallback path list --
# verified by exporting at dev and reading the emitted file, not assumed.
scripts/data/regenerate_cluster_manifests.py

# ============================================================================
# NOT SHIPPED -- each a decision, recorded so the export cannot silently regain it
# ============================================================================
# CLAUDE.md            defect counts, issue numbers, internal process; a public
#                      dev guide replaces it
# TODO/                the 249-row production plan; compiled from the 16 audit
#                      dossiers restored to docs/audits/design_compliance/ below
# tools/               the generator behind TODO/production_plan/ (compile_plan.py,
#                      annotations.json) -- it has no meaning without that corpus.
#                      EXCEPT tools/docs/generate_key_reference.py, allowed above,
#                      because a generated page that ships without its generator
#                      documents a command the reader cannot run
# .agent/  .claude/    design axioms and 16 skills carrying internal audit intel
# experiments/         1,555 files minus templates/ above; also the blast radius
#                      for the inert-knob surface, which the drop removes wholesale
#   (4 files under experiments/inference/ carry a double quote in their
#    FILENAME. They are excluded by the experiments/ decision above, not by
#    a rule of their own -- there is no directory whose name is quoted. That
#    misreading came from parsing ls-tree without -z, which quotes the whole
#    path and so appears to create a top-level root named '"experiments'.)
# scratch/ runners/    explicitly not production surface
# data/                dataset payloads
# paper/               JOSS draft -- a separate deadline
# docs/audits/ docs/superpowers/ docs/presentation/ docs/lean/ docs/analysis/
# docs/deep_papers_*   internal analysis; also where the fastMRI-derived figures live.
#   Still excluded now that a curated docs/ set ships: the allowances above name
#   pages and prefixes one by one, so nothing here is reachable by construction.
# docs/api/            175 autoapi pages; the public site uses docs/modules/ instead
# docs/contributing/   internal process (this export, the label taxonomy, the CI
#                      ratchet). docs/cluster_verification.rst likewise -- it
#                      documents one site's cluster. All four are why the two indexes
#                      are split: docs/index.rst names only pages that ship, and
#                      docs/index_internal.rst (`:orphan:`, not allowlisted) is the
#                      only page that reaches these.
#   docs/audits/design_compliance/ was deleted by PR #1587 and RESTORED, because it
#   is the compiler input for TODO/production_plan/ -- compile_plan.py reads nothing
#   else, and without it the generator silently printed 'TOTAL 0' and exited 0. It is
#   excluded by the same decision as TODO/ and tools/: the three are one corpus and
#   shipping any of them alone is meaningless.
#
#   Enforced by this file being FAIL-CLOSED, deliberately NOT by a '!docs/audits/'
#   denial. docs/ ships as three exact filenames, so nothing allows these paths in
#   the first place; export_public_tree.py only marks a denial used once it cancels
#   an otherwise-shipping file, so such a line would never be used, would land in
#   'dead', and would make --strict exit 2. A dead denial breaks the gate it was
#   meant to document -- see docs/contributing/public_export.rst on dead patterns.
# audit_file.txt  EXPERIMENTS_YAML_CATALOG.md  RELEASE_NOTES_v6_1.md
# mata_eval.sbatch  cluster_update.sh  run_eda.sh  clear_cache.sh
# download_datasets.sh manifest.yaml mutmut_config.py
# .aiexclude .cgcignore .vscode/
# (.github/ now SHIPS -- see the "CI workflows" block above. F's condition is met
#  by the two denials there. This line is kept as the record of why it was held
#  back, not as a current exclusion.)
