Detecting missing data management plans programmatically
A missing or hollow data management plan is one of the quietest ways a grant proposal earns an administrative rejection: the science is untouched, but the National Institutes of Health (NIH) or National Science Foundation (NSF) intake screen finds no compliant Data Management and Sharing Plan attached and withdraws the application before review. Since 2023 the NIH Data Management and Sharing (DMS) Policy requires the plan on nearly every application, and NSF has required a two-page Data Management Plan on every proposal for over a decade, so its absence — or a version that names no repository and no sharing timeline — is a preventable failure a validator should catch during assembly. This guide builds that detector as a specialization of required section mapping: first locate the plan, then confirm it actually contains the elements the policy mandates rather than an empty heading.
The subtlety is that presence is necessary but not sufficient. A proposal can carry a section titled “Data Management Plan” that says only “data will be shared upon request” and still fail, because the policy demands specific content — the types of data, the standards, where and how they will be shared, and on what timeline. Detection therefore runs in two stages: a structural pass that finds the section, and a content pass that checks it against a required-element set.
Phase 1 — Locate the plan by bookmark, then by heading
Federal submission packages are assembled from separately uploaded attachments, and the data plan is usually its own document, so the fastest and most reliable path is the upload manifest or the PDF outline tree rather than a full-text scan. The first phase resolves the section’s location and confirms it exists at all, escalating through a fallback chain when the primary lookup misses — the same graceful-degradation pattern the parent section mapper uses so a single template quirk does not cascade into a false “missing” verdict.
Implementation steps:
- Check the attachment manifest. In a structured package, the plan is a named component. If the manifest lists a Data Management (and Sharing) Plan document, resolve directly to it.
- Read the PDF outline. When the plan is embedded in a combined PDF, walk the bookmark tree (
/Outlines) for a node whose title matches the canonical nomenclature for the agency. - Fall back to a heading scan. When bookmarks are flattened, search the extracted text for a line-anchored heading, drawing heading candidates from the NLP section boundary detection stage so a phrase buried mid-paragraph is not mistaken for the section start.
- Route a miss to review, never to a pass. If every strategy misses, the plan is reported absent and routed for human confirmation — a missing anchor is a blocking finding, not a silent clearance.
Phase 2 — Validate the required elements by section and keyword checks
Locating the section clears the low bar; the policy sets a higher one. Both agencies require the plan to address a specific set of elements, and a present section that omits any of them is non-compliant. The content pass models the required elements as data and checks the located section text for each, treating a plan as complete only when every mandated element is evidenced.
Model the policy and the detector together so a malformed configuration fails at construction, using Pydantic v2 rather than the deprecated validator syntax:
from __future__ import annotations
import re
from enum import Enum
from pydantic import BaseModel, field_validator
class DMPElement(str, Enum):
DATA_TYPES = "data_types" # what data, in what formats and volume
STANDARDS = "standards" # metadata and documentation standards
SHARING = "sharing" # how and with whom data will be shared
REPOSITORY = "repository" # the named archive or repository
TIMELINE = "timeline" # preservation and access timeline
class DMPPolicy(BaseModel):
"""One agency's data-plan requirements as validated data."""
agency: str
heading_patterns: tuple[str, ...] # regexes that name the section
required_elements: frozenset[DMPElement]
max_pages: int | None = None # policy page cap, if any
# Keyword cues that evidence each element in the section text.
element_cues: dict[DMPElement, tuple[str, ...]]
@field_validator("heading_patterns")
@classmethod
def _patterns_compile(cls, patterns: tuple[str, ...]) -> tuple[str, ...]:
for p in patterns:
re.compile(p) # raises re.error on a bad regex
return patterns
class DMPVerdict(BaseModel):
agency: str
present: bool
page_count: int | None = None
missing_elements: list[DMPElement] = []
@property
def is_compliant(self) -> bool:
return self.present and not self.missing_elements
The detector locates the section, then scores each required element against its keyword cues. A cue hit is deliberately conservative — it evidences that an element was addressed, not that it was addressed well — because the validator’s job is to catch omissions, not to grade prose:
def locate_plan(text: str, policy: DMPPolicy) -> int | None:
"""Return the character offset of the plan heading, or None if absent."""
for pattern in policy.heading_patterns:
match = re.search(pattern, text, re.IGNORECASE | re.MULTILINE)
if match is not None:
return match.start()
return None
def validate_plan(text: str, policy: DMPPolicy, page_count: int | None = None) -> DMPVerdict:
"""Confirm the plan is present and addresses every required element."""
offset = locate_plan(text, policy)
if offset is None:
return DMPVerdict(agency=policy.agency, present=False,
missing_elements=sorted(policy.required_elements, key=str))
section = text[offset:] # scope the scan to the plan
missing: list[DMPElement] = []
for element in policy.required_elements:
cues = policy.element_cues.get(element, ())
if not any(re.search(rf"\b{re.escape(c)}\b", section, re.IGNORECASE) for c in cues):
missing.append(element)
return DMPVerdict(
agency=policy.agency, present=True,
page_count=page_count, missing_elements=missing,
)
Scoping the scan to the text at and after the heading offset keeps a repository named in an unrelated Facilities section from counting as coverage of the data plan — the element must be evidenced inside the plan. Because the verdict names exactly which elements are absent, it feeds cleanly into automated checklist generation, which turns each missing element into a routed deficiency line for the principal investigator rather than a bare “plan incomplete.”
Phase 3 — Agency overrides: NIH DMS policy, NSF DMP, and DoD data rights
The three agencies name the plan differently, require different elements, and cap length differently, so the policy object is agency-specific and the detector generic. The NIH DMS Plan follows the 2023 policy and is recommended to stay within two pages; the NSF Data Management Plan is a required two-page supplementary document; a Department of Defense (DoD) submission reframes the concern around data-rights assertions under its acquisition regulations rather than a public-sharing mandate.
| Concern | NIH (DMS Plan) | NSF (DMP) | DoD (data rights) |
|---|---|---|---|
| Section name | Data Management and Sharing Plan | Data Management Plan | Data management & rights assertions |
| Governing policy | 2023 DMS Policy | Proposal & Award Policies & Procedures Guide (PAPPG) | DFARS data-rights clauses, per solicitation |
| Length cap | ~2 pages (recommended) | 2 pages (required) | Per-solicitation |
| Sharing emphasis | Public sharing + repository | Sharing + dissemination | Rights assertion, limited/restricted markings |
| Required repository | Named data repository expected | Repository or access method | Delivery to the contracting agency |
| Absence outcome | Application not reviewed | Return without review | Non-conforming proposal |
NIH_DMS = DMPPolicy(
agency="NIH",
heading_patterns=(r"^\s*Data\s+Management\s+and\s+Sharing\s+Plan\b",
r"^\s*DMS\s+Plan\b"),
required_elements=frozenset(DMPElement), # NIH expects all five
max_pages=2,
element_cues={
DMPElement.DATA_TYPES: ("data type", "dataset", "metadata"),
DMPElement.STANDARDS: ("standard", "format", "documentation"),
DMPElement.SHARING: ("share", "sharing", "access", "disseminat"),
DMPElement.REPOSITORY: ("repository", "archive", "database"),
DMPElement.TIMELINE: ("timeline", "preserv", "retention", "duration"),
},
)
NSF_DMP = DMPPolicy(
agency="NSF",
heading_patterns=(r"^\s*Data\s+Management\s+Plan\b",),
required_elements=frozenset(
{DMPElement.DATA_TYPES, DMPElement.STANDARDS,
DMPElement.SHARING, DMPElement.REPOSITORY} # timeline folded into sharing
),
max_pages=2,
element_cues=NIH_DMS.element_cues, # cue vocabulary is shared
)
Two overrides deserve emphasis. NIH’s two-page figure is a recommendation the plan is expected to respect, so it is carried as max_pages and checked, but an overage is a warning rather than the hard block an over-length narrative is — a distinction the threshold tuning for compliance layer sets, not the detector. NSF’s required-element set is narrower and reframes preservation timeline as part of its sharing statement, so the same detector with a different policy produces the agency-correct verdict. The NSF path also intersects the program-specific structure covered in mapping mandatory sections for NSF CAREER proposals, where the data plan is one required supplementary document among several.
Phase 4 — Validation and pytest
Because location and element checking are pure text functions, they test deterministically against constructed inputs — no PDFs, no network, no clock.
import pytest
def test_absent_plan_is_flagged_present_false() -> None:
text = "Specific Aims\n...\nResearch Strategy\n..." # no data plan heading
verdict = validate_plan(text, NIH_DMS)
assert not verdict.present
assert not verdict.is_compliant
assert set(verdict.missing_elements) == set(DMPElement)
def test_present_but_hollow_plan_lists_missing_elements() -> None:
text = (
"Data Management and Sharing Plan\n"
"We will collect datasets and share them; formats follow community standards."
) # no repository, no timeline cues
verdict = validate_plan(text, NIH_DMS)
assert verdict.present
assert DMPElement.REPOSITORY in verdict.missing_elements
assert DMPElement.TIMELINE in verdict.missing_elements
def test_complete_nih_plan_passes() -> None:
text = (
"Data Management and Sharing Plan\n"
"Data types: imaging datasets with metadata. Standards: BIDS format and documentation. "
"Sharing: data will be shared via a public repository. "
"Preservation timeline: retention for ten years."
)
verdict = validate_plan(text, NIH_DMS)
assert verdict.is_compliant
def test_nsf_policy_does_not_require_a_separate_timeline() -> None:
text = (
"Data Management Plan\n"
"Datasets with metadata standards will be shared through a named repository."
)
verdict = validate_plan(text, NSF_DMP)
assert verdict.is_compliant # NSF folds timeline into sharing
A manual acceptance pass confirms what the fixtures cannot: that a repository named only in the Facilities section does not count as data-plan coverage, that the heading match anchors on a real heading rather than a mid-sentence mention, that an NIH plan running past two pages surfaces as a warning while a missing element surfaces as a blocker, and that an unreadable attachment routes to review instead of silently reporting the plan absent. When those hold, the detector converts one of the most common administrative-rejection causes into a finding the proposal team can clear days before the deadline rather than discovering after the portal closes.
Frequently asked questions
Is finding the section enough to pass the check?
No. A section titled “Data Management Plan” that says only “data will be shared upon request” is present but non-compliant, because the policy requires specific elements — the data types, standards, sharing provisions, a named repository, and a preservation timeline. The detector runs a content pass over the located section and reports exactly which required elements are missing, so a hollow plan fails just as a missing one does.
How does the NIH DMS Plan differ from the NSF Data Management Plan here?
They differ in name, required-element set, and emphasis. The NIH Data Management and Sharing Plan follows the 2023 DMS Policy and is expected to address all five elements within about two pages; the NSF Data Management Plan is a required two-page document with a narrower element set that folds the preservation timeline into its sharing statement. The same detector produces the agency-correct verdict by swapping the policy object.
What if the plan is embedded in a combined PDF instead of a separate attachment?
The detector escalates through a fallback chain: the attachment manifest first, then the PDF bookmark tree, then a line-anchored heading scan over the extracted text using heading candidates from the section-boundary detection stage. Only when every strategy misses is the plan reported absent, and that verdict is routed for human confirmation rather than treated as a silent clearance.
Should an over-length data plan block submission?
Not on its own. NIH’s two-page figure is a recommendation carried as a warning, whereas a missing required element is a blocker. The detector records the page count and the missing elements separately, and the threshold-tuning layer decides which severity each carries, so an over-length plan and a hollow one are not conflated.
Related
- Mapping mandatory sections for NSF CAREER proposals — the data plan as one required section among a program’s full set.
- NLP section boundary detection — supplies the heading candidates the fallback scan relies on.
- Automated checklist generation — turns each missing element into a routed deficiency line for the PI.
- Threshold tuning for compliance — decides whether an over-length or hollow plan is a warning or a blocker.
Up one level: Required Section Mapping