Enveda Phase 0 independent audit — 2026-09-17
Verdict: G0 FAIL / REOPEN REQUIRED
The previous PHASE0_CORE_AND_MANIFESTS_PASS and TODO's closed-G0 claim are not supported by the substantive checks below. Existing implementation and manifests were not changed. This report supersedes their audit conclusion, not their on-disk status fields. Do not treat downstream identity-derived artifacts as scorer-compatible until remediation and independent rebuild verification.
Fresh verification
9423f90ec08b941a9646b535bf0606183e86ff0c5a297978f5a70693d41bcd98.Findings, severity ordered
F1 — CRITICAL: inconsistent tautomer settings split an equivalent molecule across folds
Sources: src/identity.py:15-17,27-28,38-39; src/split_manifest.py:64-77; reports/phase0-identity-probe-2026-09-17.json.
identity14() restricts the tautomer enumerator to 100 transforms; canonical_smiles() uses the RDKit default 1,000 transforms. Both allow 1,000 tautomers. A real cached training structure returns:
identity14(s) = YYBZEWCJTIGSEMidentity14(canonical_smiles(s)) = ZZZRUAITSXLWBHThe manifest puts the first key in fold 4, 102 rows, and the second key in fold 2, 52 rows. All three cached SMILES belonging to those two keys collapse to ZZZRUAITSXLWBH with the default RDKit canonicalizer. Thus an actual default-canonicalizer identity crosses validation folds, not just a theoretical risk.
The diagnostic tested the first 100 cache entries containing at least six O characters and found this example. This is a targeted diagnostic, not an estimate of corpus-wide error prevalence. The exact official executable scorer was not available in the inspected snapshots; its documented operation and default-RDKit comparison must not be confused with verified official-source parity. Even independently of that uncertainty, the two project helpers demonstrably violate the plan's one-identity-path requirement.
Required: confirm canonicalizer settings against the official scorer when available; use one shared implementation; add complex-tautomer consistency tests; rebuild identity cache, splits, external overlap/structure/scaffold manifests, reference identity arrays, masks and candidate mappings. Do not patch only the function and reuse old caches.
F2 — HIGH: formate adduct mass is wrong by 1.007278304 Da
Source: src/adducts.py:18; failing probe reports/test_phase0_audit_regressions.py:10-13.
For [M+CH2O2-H]-, expected delta is approximately 44.998202837 Da (neutral formic acid minus a proton). The code uses 44.998201 - 1.007276466621 = 43.990924533 Da, effectively subtracting the proton twice.
A precursor corresponding to neutral mass 300 Da is decoded as 301.007278304 Da. Live column scans found 83,188 training rows and 31 visible-test spectra with this adduct. This can break cross-adduct/structure-mass matching; it does not imply every same-adduct retrieval necessarily fails, since both query and reference may share the offset.
Why tests missed it: the round-trip test uses the same wrong constant in both directions (tests/test_phase0.py:23-26).
Required: correct the delta, test all ten adducts against independently derived masses, rebuild affected mass indexes and derived retrieval outputs.
F3 — HIGH: required scorer/evaluator tests are missing
Sources: EXECUTION-PLAN.md:127-142; tests/test_phase0.py:1-31; TODO.md Phase 0 scorer-tests checkbox.
The checked-in Phase 0 suite tests invalid strings, one stereo pair, acetone tautomer equivalence, adduct round-tripping and unknown-adduct rejection. It does not test the claimed rank cutoff or a submission-level MRR@25 contract: ranks 1/25/26, first correct guess, semicolon parsing, duplicate/null/missing IDs, >25 candidates, constitutional isomers, equivalence-aware deduplication or dynamic IDs.
Phase 1 contains inline reciprocal-rank arithmetic; that is not a substitute for the missing tested scorer contract. The Phase 0 completed checkbox overstates implemented coverage.
Required: implement and test the molecule-level scorer and submission contract; keep unknown official edge cases explicit rather than inventing behavior.
F4 — HIGH: the three frozen validation regimes are not fully materialized
Sources: EXECUTION-PLAN.md:146-165; src/strict_validation.py:21-29,64-82,99-120; src/validate_phase0.py:91-94.
library_ceiling excludes only the exact row (src/phase1_baseline.py:181), not duplicate acquisitions, and remains a ceiling diagnostic.build(). Reconstructing them depends on a mutable, unversioned cache, first-encountered SMILES without SQL ordering, and raw-SMILES scaffold extraction. scaffold_folds_exclusive is hard-coded True, not independently verified. Aggregate count conservation cannot establish the exact frozen held-out population or external candidate scaffold exclusions.Required: persist the A acquisition/duplicate exclusions and C identity/scaffold/fold assignments with data, code, algorithm and dependency hashes; assert disjointness against the actual chosen reference/candidate pools. The bounded PubChem scope itself is not a defect; using all training molecules in scaffold CV is also not inherently wrong.
F5 — HIGH: the gate checks declarations and aggregates, not critical invariants
Source: src/validate_phase0.py:54-75.
The fresh validator PASS is misleading because it:
scaffold_folds_exclusive boolean.config.validation.group_key against the split.Required: turn these into fail-closed executable checks, verify actual inputs and emitted mappings, and include source/config/dependency fingerprints in every result. Hashing an artifact alone is not verification of its correctness.
F6 — MEDIUM, mandatory before rebuilding: config and cache provenance are unsafe
Sources: configs/phase0.json:8; src/split_manifest.py:37-44,64-77.
Config declares group_key: inchikey14; manifest declares scorer_identity14. The cache schema is only (smiles PRIMARY KEY, identity): no RDKit version, canonicalizer settings or implementation fingerprint. A checkpoint is resumed without binding it to data hash, identity algorithm, seed/fold configuration or cache fingerprint. Changing the identity function alone can therefore silently reuse stale identities or completed checkpoint statistics.
Required: align config, use a new versioned cache/checkpoint namespace, reject provenance mismatches, and regenerate rather than silently mixing versions.
Additional observations / limits
reports/.Minimum reopening/closure sequence
1. Confirm and unify scorer identity settings; correct mass conversion; add independent regression and full scorer-contract tests.
2. Create new fingerprinted cache/checkpoint artifacts and rebuild every dependent identity/mass artifact.
3. Freeze actual A acquisition exclusions, B structure cohort and C scaffold-fold assignments under the corrected identity convention.
4. Strengthen the gate, independently rescan all row mappings/disjointness, verify artifact hashes and capture runtime dependency versions.
5. Re-run G0 and only then certify Phase 1 evaluation/submission outputs. Engineering may continue, but no current downstream metrics should be promoted as clean validation evidence.
Reproduction / evidence
From project root:
.venv-rdkit2026/bin/python -m pytest -q tests.venv-rdkit2026/bin/python -m pytest -q reports/test_phase0_audit_regressions.py.venv-rdkit2026/bin/python reports/audit_phase0_20260917.py.venv-rdkit2026/bin/python -m src.validate_phase0 --output reports/phase0-gate-rerun-2026-09-17.jsonThe audit script consumes the saved bounded identity diagnostic; it independently rechecks its cross-fold example against the cache and default canonicalizer. Do not interpret it as a full default-identity rescan.
Evidence files:
reports/phase0-existing-tests-2026-09-17.logreports/phase0-regressions-2026-09-17.logreports/phase0-independent-evidence-2026-09-17.json (includes source/config hashes)reports/phase0-identity-probe-2026-09-17.jsonreports/phase0-adduct-counts-2026-09-17.jsonreports/phase0-gate-rerun-2026-09-17.json