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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
Locating and validating a data management plan A vertical flow. The detector first locates the data management plan by bookmark with a heading fallback, then a decision node asks whether the section is present. If absent, the flow branches left to a blocking MISSING PLAN outcome that is an administrative reject. If present, the flow continues down to an element-validation panel listing five required elements: data types and metadata standards pass, sharing and access provisions pass, a named repository or archive is absent, preservation and retention timeline passes, and format and documentation standards pass. Because one of five required elements is missing, the panel resolves to a blocking INCOMPLETE verdict noting the repository element was not named. Locate DMS / DMP section bookmark → heading fallback Section present? no MISSING PLAN administrative reject yes Validate required elements Data types & metadata standards Sharing & access provisions Named repository / archive Preservation & retention timeline Format & documentation standards 1 of 5 required elements missing Verdict: INCOMPLETE (blocker) repository element not named
Detection is two-stage: locate the plan, then confirm it carries every mandated element — a present-but-hollow plan fails just as a missing one does.

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:

python
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:

python
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
python
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.

python
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.

Up one level: Required Section Mapping