feat: read requirements.txt contents to judge pinning (feature 037) - #443
Merged
Marc-cn merged 2 commits intoSep 20, 2026
Merged
Conversation
…itdevorg#427, darnitdevorg#428) Both reported by @Jaydeep869 against darnit-reproducibility; both root causes are in the framework. ## darnitdevorg#427 -- plugin handlers never registered outside the MCP path `darnit audit --framework reproducibility` emitted "handler 'repro_deps_pinned' not found in registry" for all five of the plugin's controls, which then fell through to `manual` and reported WARN. The WARN text is "Could not automatically verify - manual verification required" -- indistinguishable from a control that genuinely could not be determined, which is why this read as working software. Three separate defects had to be fixed: 1. `cmd_audit` resolved the framework for loading controls but never passed `framework_name` to `run_sieve_audit`, so the driver had no idea which framework it was running. openssf-baseline masked this because its controls use only built-in handlers, which need no plugin registration. 2. `impl.register_handlers()` was called in exactly one place -- `server/factory.py`, the MCP server path. Every other entry point got no plugin handlers. Added `core.discovery.register_implementation_handlers()` and called it from `run_sieve_audit`, which is the shared driver both the CLI and the MCP audit tool go through. It runs regardless of whether `controls` was supplied, since a caller passing pre-loaded specs still needs the handlers those specs reference. 3. The two in-tree plugins that ship custom handlers implement `register_sieve_handlers()`, not the `register_handlers()` that CLAUDE.md documents and that darnit-baseline implements. They worked only because their `register()` entry point self-registers during discovery -- and discovery is cached, so any caller that warmed the cache earlier in the process left the handlers unregistered. That timing dependence is why the bug presented as 3-of-5 handlers missing on one run and 5-of-5 on the next. The new helper accepts either spelling so registration is explicit and cache-independent. The reporter's proposed fix (calling `discover_implementations()` in `cmd_audit`) addresses the symptom but not the cause: discovery does not register handlers, and relying on it re-firing `register()` is the cache-dependent side channel above. Reconciling the two method names across plugins is left alone here -- that touches three packages and deserves its own change. The tolerance is pinned by tests so it stays deliberate rather than accidental. ## darnitdevorg#428 -- non-deterministic control order `merge_configs` collected control IDs into a `set`, so iteration order was randomized per PYTHONHASHSEED and the same repo audited twice listed its controls differently. Five consecutive runs produced five orderings. Deduped through `dict.fromkeys` instead. `dict` keys already carry TOML declaration order, which groups controls by domain the way the framework author wrote them; sorting would have been stable too but would have discarded that grouping. ## Verification Before: 5 handlers missing, order differed across all 5 runs. After: 0 handlers missing across 3 runs, order stable, and RE-01.01 returns a real verdict for the first time via CLI. 13 new tests. Full suite: 2918 passed, 14 skipped. Note on the test command: `--ignore=tests/darnit/parity` skips parity tier 1, which CI runs. The correct local invocation is `--ignore=tests/darnit/parity/tier2` -- tier 2 is manual-dispatch and needs API keys, tier 1 runs on every PR. Signed-off-by: Michael Lieberman <mlieberman85@gmail.com>
mlieberman85
force-pushed
the
037-pinned-requirements-detection
branch
from
September 17, 2026 21:12
e133d56 to
e0e8e20
Compare
…arnitdevorg#429) `repro_deps_pinned` judged dependency pinning by filename and never opened the files it judged. A `requirements.txt` produced by `pip-compile --generate-hashes` received the same hard FAIL as one containing a bare `numpy`, with a message ("Dependency manifests found but no lock files") that was accurate about what the check looked at and misleading about what it concluded. The control now reads the file when no lock file is present: hash-pinned every requirement `==` plus a hash PASS version-pinned every requirement `==`, no hashes WARN unpinned any open specifier FAIL, names an offender not-inspectable unreadable, undecodable, or an unresolved `-r` include FAIL, says so WARN rather than PASS for the version-pinned case is deliberate and has a cost worth stating: because WARN counts as FAIL for compliance arithmetic (Principle II), a project that pins every direct dependency still cannot reach compliance on this control without a lock file. Its transitive dependencies resolve at install time, so the build is not reproducible and the control cannot honestly assert that it is. A commit-SHA VCS reference is exactly identified but is never hash evidence, so it caps a file at WARN. pip cannot enforce hashes on a git reference, so a file containing one is not a require-hashes file and must not borrow that tier's justification. Framework change: HandlerResultStatus gains WARN ------------------------------------------------ A handler could report PASS, FAIL, INCONCLUSIVE or ERROR. One that had read the evidence and found it insufficient had to return INCONCLUSIVE, which sends the pipeline onward and eventually produces a WARN carrying the fixed string "Could not automatically verify - manual verification required". The operator then reads, about a file the tool parsed successfully, that the tool could not verify it. WARN is now a conclusive outcome carrying the handler's own message. It is gated by the RFC-0001 Stage 1 authority rule exactly as PASS and FAIL are: a suggestive step cannot manufacture a WARN that halts verification any more than it can manufacture a PASS. INCONCLUSIVE behaviour is unchanged, and the all-inconclusive fallthrough still produces its WARN for controls that exhaust their passes. No existing handler returns the new outcome. Verified mechanically rather than argued: 213 controls across 6 framework/repo pairs are captured twice in the same environment, once normally and once with the WARN outcome routed the way it was routed before this change, and the two agree exactly. Comparing against a stored golden was tried first and abandoned -- control statuses depend on the environment as much as on the code, so a golden pins the runner rather than the change. `packaging` is declared by darnit-reproducibility rather than inherited. darnit-core imports it at runtime in two places without declaring it anywhere; that is filed as darnitdevorg#441 and not fixed here. Scope held deliberately: file discovery is unchanged (root `requirements.txt` only), other loose manifests are still judged by presence, and confidence stays 0.8 everywhere. Closes darnitdevorg#429 Refs darnitdevorg#441, darnitdevorg#442 Signed-off-by: Michael Lieberman <mlieberman85@gmail.com>
mlieberman85
force-pushed
the
037-pinned-requirements-detection
branch
from
September 17, 2026 21:23
e0e8e20 to
643242c
Compare
Collaborator
|
Tested on Linux, after #439 merged.All five requirements.txt shapes match the table:. |
This was referenced Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #429.
repro_deps_pinnedjudged dependency pinning by filename and never opened the files it judged. Arequirements.txtproduced bypip-compile --generate-hashesreceived the same hard FAIL as one containing a barenumpy. Reported by @Jaydeep869 while evaluatingdarnit-reproducibilityagainst real repositories.What the control does now
When no lock file is present, it reads the file.
requirements.txt==+--hash===, no hashes-rLock files still decide first and their contents are never consulted.
The WARN tier has a cost, and it is intentional
Because WARN counts as FAIL for compliance arithmetic (Principle II), a project that pins every direct dependency still cannot reach compliance on this control without adopting a lock file. Its transitive dependencies resolve at install time, so the build is not reproducible and the control cannot honestly assert that it is. For a framework whose subject is reproducibility that is the correct message rather than an unfortunate side effect, but it will generate pushback from users who feel they have done the work.
A commit-SHA VCS reference is exactly identified but is never hash evidence, so it caps a file at WARN. pip cannot enforce hashes on a git reference, so a file containing one is not a require-hashes file and must not borrow that tier's PASS.
Framework change:
HandlerResultStatusgainsWARNThis is the part worth reviewer attention, because it changes what any handler is allowed to conclude.
A handler could report PASS, FAIL, INCONCLUSIVE or ERROR. One that had read the evidence and found it insufficient had to return INCONCLUSIVE, which sends the pipeline onward and eventually produces a WARN carrying the fixed string
"Could not automatically verify - manual verification required". The operator then reads, about a file the tool parsed successfully, that the tool could not verify it -- the same confusion #427 was about, arriving by a different route.WARN is now conclusive and carries the handler's own message. Gated by the RFC-0001 Stage 1 authority rule exactly as PASS and FAIL are:
PASSFAILWARNINCONCLUSIVEThe WARN row is the PASS/FAIL row. A suggestive step cannot manufacture a WARN that halts verification any more than it can manufacture a PASS. INCONCLUSIVE behaviour is unchanged, and the all-inconclusive fallthrough still produces its WARN for controls that exhaust their passes.
docs/architecture/framework-design.mddocuments the outcome and the table, per Development Workflow item 3. Note thatvalidate_sync.pycannot check this --validate_pass_types_syncgreps a hardcoded list of seven handler names againstbuiltin_handlers.py, so it passes regardless. A test asserts the documentation instead.Verification
SC-008 (no existing control's status changes): 213 controls across 6 framework/repo pairs are captured twice in the same environment -- once normally, once with the WARN outcome routed the way it was routed before this change -- and the two agree exactly.
A committed golden was tried first and is worth mentioning because it failed in CI while passing locally. Seven OSPS controls resolve FAIL on a developer machine and WARN on a GitHub runner, so a golden of absolute control statuses pins the environment as much as the code. The differential capture has no such problem: whatever the environment does, it does identically to both runs and cancels.
SC-004 / SC-005 (lock-file and no-dependency paths byte-identical, including confidence): same ordering.
3207 tests pass,
ruff check .clean,validate_sync.pypasses.Two bugs found along the way, filed and not fixed here
#442 -- controls leak across frameworks within a process. A
reproducibilityaudit returns 5 controls in a clean process and 71 (65 of them OSPS baseline) if anopenssf-baselineaudit ran earlier in the same process. The MCP server is long-lived, so this affects the product surface, not just tests. It surfaced because the in-process baseline capture reported 46% of the corpus as nondeterministic -- none of it flaky, all of it this leak.baseline_capture.pyruns each pair in a subprocess, which is a workaround rather than a fix.#441 --
darnit-coreimportspackagingat runtime without declaring it.composition.py:363andframework_schema.py:1445;uv pip show packagingreportsRequired-by: pytest. This PR declarespackagingfordarnit-reproducibility's own use, which does not cover core's call sites.Also worth knowing:
run_checkswrites a.project/directory into the repository it audits. The baseline capture copies fixtures into scratch space before auditing for that reason -- auditing them in place leaves untracked files in the tree, which is what broke CI on #435.Scope held deliberately
File discovery is unchanged (root
requirements.txtonly --requirements-dev.txtandrequirements/base.txtstay undiscovered), other loose manifests are still judged by presence alone, confidence stays 0.8 everywhere, and-r/-cincludes are not followed. Each is a requirement with a test asserting the non-change.Stacked on #439
This branch is built on
fix-plugin-handler-registration(#427, #428), which is still open, so its commit appears here until that merges. Rebasing off it is not an option: the committed SC-008 baseline was captured with it present, and without itrepro_deps_pinneddoes not execute via the CLI path at all. Merge #439 first.