Skip to content

Add template compliance checking (port template matching engine from nanodash) #91

Description

@tkuhn

Summary

Nanodash has mature machinery for filling nanopubs from templates and matching existing nanopubs against templates. We should move/replicate the core of this capability into nanopub-java, so that any client (CLI, bots, services) can check whether a nanopub complies with its declared template(s) prior to publishing.

The migration has in fact quietly started: nanodash's template code already imports org.nanopub.vocabulary.NTEMPLATE from nanopub-java, and nanodash's Template.java carries a TODO about moving further terms here.

What the capability consists of in nanodash today

Three layers with very different portability:

1. Template parsing — portable almost verbatim. Template.java (~1080 lines) parses a template nanopub into placeholder/statement/constraint maps. Dependencies are RDF4J, nanopub-java itself, and three small nanodash bits: nanopub retrieval, HTML sanitization (cosmetic), and lookup-API support (only needed for guided-choice autocompletion, which a checker doesn't need). Swapping retrieval for GetNanopub (or an injectable resolver interface) makes it port cleanly. The KPXL_TERMS role-direction pins need two IRIs added to the existing KPXL vocabulary class.

2. Statement preparation — portable with one design decision. Nanodash's ValueFiller (~220 lines) takes the nanopub-to-check, rewrites its own URIs into template-local form, and — importantly for checking — encodes the whitelist of pubinfo triples that are not expected to come from the template (timestamp, signature block, supersedes, labels, wasCreatedFromTemplate itself, role-direction pins, …). That filter list is effectively the compliance semantics for the pubinfo graph and must move with the engine. Its only awkward dependency is a static label-cache side effect, which is trivially droppable.

3. The unification engine — the actual work. The part that answers "does this nanopub instantiate the template?" is currently fused with the Wicket UI:

  • TemplateContext binds placeholder values via Map<IRI, IModel<?>> (Wicket models) and pulls the creator from the session.
  • StatementItem.fill() / RepetitionGroup.assignParts() implement the backtracking assignment of nanopub triples to template statements, including repeatable-statement trial groups, rollback of partial bindings, and the __N narrow-scope repetition suffixes.
  • The per-placeholder-type constraint checks (regex, prefix stripping, datatype/language-tag agreement, restricted-choice membership, local-resource namespace handling, auto-escape decoding) live in each form component's isUnifiableWith implementation (IriTextfieldItem, LiteralTextfieldItem, RestrictedChoiceItem, etc.).

None of that logic is conceptually Wicket-dependent — the models are just used as mutable binding slots — but it has to be extracted and re-hosted.

Proposed design for nanopub-java

A new package (e.g. org.nanopub.templates) with roughly:

  • Template — ported from nanodash, with a pluggable NanopubResolver instead of nanodash's retrieval utility (needed for the template itself, possibleValuesFrom nanopubs, and nothing else).
  • BindingContext — replaces Map<IRI, IModel<?>> with a plain Map<IRI, Value> plus the introduced/embedded-IRI bookkeeping from TemplateContext. The creator placeholder binds to the checked nanopub's signer/creator instead of a session user.
  • PlaceholderMatcher hierarchy — one class per placeholder type, containing the isUnifiableWith logic lifted out of the *Item components. Mechanical but fiddly; each component mixes constraint logic with model plumbing that has to be teased apart.
  • TemplateMatcher — a headless port of the StatementItem.fill + RepetitionGroup.assignParts backtracking, operating on binding snapshots instead of Wicket model snapshots. The snapshot/restore pattern actually gets simpler headless (copy a map, no clearInput() concerns).
  • TemplateComplianceChecker — the new top-level API: read ntemplate:wasCreatedFromTemplate / ...FromProvenanceTemplate / ...FromPubinfoTemplate from pubinfo (there can be multiple pubinfo templates), fetch each declared template at its exact trusty version, run the matcher per graph, and produce a report: unmatched non-optional template statements, leftover nanopub statements not covered by any template or the pubinfo whitelist, placeholder constraint violations, missing required pubinfo elements, and target-nanopub-type presence.
  • CLI — a CheckTemplateCompliance CliRunner alongside CheckNanopub (or a flag on it), so it slots into the existing np check-style tooling.

Decisions and tricky parts

  • Strictness semantics. Nanodash's fill machinery is deliberately lenient: unification failures are logged and skipped, and unmatched statements just land in an unusedStatements list (shown as a warning in the UI). A pre-publish checker must decide what's an error vs. a warning. Suggestion: non-optional template statement unmatched → error; leftover assertion statements → error (the nanopub claims template provenance it doesn't have); leftover pubinfo statements → warning (users legitimately add extra pubinfo).
  • The backtracking engine is subtle. Recent nanodash history (backtracking in assignParts, trial-group stale-model fix, per-statement isolation in TemplateContext.fill) shows this code has sharp edges around shared placeholders across repetition groups. A port needs a real test suite — ideally round-trip tests: create nanopubs from a corpus of live templates with NanopubCreator, assert the checker accepts them, plus mutation tests that it rejects perturbed ones.
  • Network access. Checking a RestrictedChoicePlaceholder with possibleValuesFrom requires fetching the values nanopub; guided-choice/API placeholders should just be validated syntactically (regex/prefix), since re-querying lookup APIs at check time is neither reliable nor meaningful.
  • Version pinning is a simplification, not a problem. Unlike nanodash (which resolves latest template versions for forms), a compliance check should use exactly the trusty-URI version declared in pubinfo — no supersedes-chain logic needed.

Migration strategy

Port the core (layers 1–3 above) into nanopub-java as the single source of truth, then — as a second step — refactor nanodash so TemplateContext/StatementItem delegate binding and unification to the library, keeping only the Wicket rendering, AJAX repetition buttons, and template caching locally. Nanodash already depends on nanopub-java, so the dependency direction works. Doing the copy without the follow-up refactor would work for a v1 but forks ~1500 lines of subtle logic that has needed several careful bug fixes recently, so the nanodash-consumes-library step should be planned even if it lands later.

Rough effort: ~1 day for the Template/ValueFiller port, 2–4 days for the headless matcher extraction plus tests (the bulk of the risk), ~1 day for the checker/report/CLI — and a separate, larger chunk for the nanodash refactor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions