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.
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.NTEMPLATEfrom nanopub-java, and nanodash'sTemplate.javacarries 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 forGetNanopub(or an injectable resolver interface) makes it port cleanly. TheKPXL_TERMSrole-direction pins need two IRIs added to the existingKPXLvocabulary 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,wasCreatedFromTemplateitself, 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:
TemplateContextbinds placeholder values viaMap<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__Nnarrow-scope repetition suffixes.isUnifiableWithimplementation (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 pluggableNanopubResolverinstead of nanodash's retrieval utility (needed for the template itself,possibleValuesFromnanopubs, and nothing else).BindingContext— replacesMap<IRI, IModel<?>>with a plainMap<IRI, Value>plus the introduced/embedded-IRI bookkeeping fromTemplateContext. The creator placeholder binds to the checked nanopub's signer/creator instead of a session user.PlaceholderMatcherhierarchy — one class per placeholder type, containing theisUnifiableWithlogic lifted out of the*Itemcomponents. Mechanical but fiddly; each component mixes constraint logic with model plumbing that has to be teased apart.TemplateMatcher— a headless port of theStatementItem.fill+RepetitionGroup.assignPartsbacktracking, operating on binding snapshots instead of Wicket model snapshots. The snapshot/restore pattern actually gets simpler headless (copy a map, noclearInput()concerns).TemplateComplianceChecker— the new top-level API: readntemplate:wasCreatedFromTemplate/...FromProvenanceTemplate/...FromPubinfoTemplatefrom 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.CheckTemplateComplianceCliRunner alongsideCheckNanopub(or a flag on it), so it slots into the existingnp check-style tooling.Decisions and tricky parts
unusedStatementslist (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).assignParts, trial-group stale-model fix, per-statement isolation inTemplateContext.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 withNanopubCreator, assert the checker accepts them, plus mutation tests that it rejects perturbed ones.RestrictedChoicePlaceholderwithpossibleValuesFromrequires 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.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/StatementItemdelegate 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/ValueFillerport, 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.