• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanopub-query / 24964097015

26 Apr 2026 06:38PM UTC coverage: 58.926%. Remained the same
24964097015

push

github

web-flow
Merge pull request #82 from knowledgepixels/feature/62-flip-backcompat-direction

fix: flip backcompat role-predicate direction to match publisher convention (#62)

385 of 740 branches covered (52.03%)

Branch coverage included in aggregate %.

1064 of 1719 relevant lines covered (61.9%)

9.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.08
src/main/java/com/knowledgepixels/query/SpacesExtractor.java
1
package com.knowledgepixels.query;
2

3
import java.util.ArrayList;
4
import java.util.Collection;
5
import java.util.Collections;
6
import java.util.Date;
7
import java.util.LinkedHashSet;
8
import java.util.List;
9
import java.util.Set;
10

11
import org.eclipse.rdf4j.model.IRI;
12
import org.eclipse.rdf4j.model.Literal;
13
import org.eclipse.rdf4j.model.Resource;
14
import org.eclipse.rdf4j.model.Statement;
15
import org.eclipse.rdf4j.model.ValueFactory;
16
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
17
import org.eclipse.rdf4j.model.vocabulary.DCTERMS;
18
import org.eclipse.rdf4j.model.vocabulary.RDF;
19
import org.nanopub.Nanopub;
20
import org.nanopub.NanopubUtils;
21
import org.nanopub.vocabulary.NPA;
22
import org.nanopub.vocabulary.NPX;
23
import org.slf4j.Logger;
24
import org.slf4j.LoggerFactory;
25

26
import com.knowledgepixels.query.vocabulary.BackcompatRolePredicates;
27
import com.knowledgepixels.query.vocabulary.GEN;
28
import com.knowledgepixels.query.vocabulary.SpacesVocab;
29

30
import net.trustyuri.TrustyUriUtils;
31

32
/**
33
 * Pure-logic extractor from a loaded {@link Nanopub} to the add-only summary
34
 * triples destined for {@code npa:spacesGraph}. Implements the per-type schema
35
 * from {@code doc/plan-space-repositories.md}.
36
 *
37
 * <p>Dispatch is by nanopub type — {@link NanopubUtils#getTypes(Nanopub)} returns
38
 * both {@code rdf:type} / {@code npx:hasNanopubType} declarations and, for
39
 * single-predicate-assertion nanopubs, the predicate itself. That means the
40
 * four predefined types ({@link GEN#SPACE}, {@link GEN#HAS_ROLE},
41
 * {@link GEN#SPACE_MEMBER_ROLE}, {@link GEN#ROLE_INSTANTIATION}) and all 14
42
 * {@link BackcompatRolePredicates backwards-compat predicates} can be detected
43
 * with a single type-set lookup.
44
 *
45
 * <p>Output: a list of RDF4J {@link Statement}s, all in the
46
 * {@link SpacesVocab#SPACES_GRAPH} named graph, that the caller writes into the
47
 * {@code spaces} repo. Deterministic and idempotent — the same nanopub always
48
 * produces the same statement set.
49
 */
50
public final class SpacesExtractor {
51

52
    private static final Logger log = LoggerFactory.getLogger(SpacesExtractor.class);
9✔
53

54
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
55

56
    private static final IRI GRAPH = SpacesVocab.SPACES_GRAPH;
9✔
57

58
    private SpacesExtractor() {
59
    }
60

61
    /**
62
     * Bundles the information a single extraction needs beyond the nanopub itself.
63
     *
64
     * @param artifactCode trusty-URI artifact code of {@code np} (used for minting
65
     *                     {@code npari:}/{@code npara:}/{@code npard:}/{@code npadef:}
66
     *                     subject IRIs).
67
     * @param signedBy     signer agent IRI from pubinfo, or {@code null} if absent.
68
     * @param pubkeyHash   hash of the signing public key, or {@code null} if absent.
69
     * @param createdAt    creation timestamp, or {@code null} if the nanopub lacks one.
70
     */
71
    public record Context(String artifactCode, IRI signedBy, String pubkeyHash, Date createdAt) {
45✔
72
    }
73

74
    /**
75
     * Runs the extractor on a loaded nanopub. Returns an empty list if the nanopub is
76
     * not space-relevant.
77
     *
78
     * @param np  the nanopub to inspect
79
     * @param ctx the extraction context
80
     * @return statements to write into {@code npa:spacesGraph}
81
     */
82
    public static List<Statement> extract(Nanopub np, Context ctx) {
83
        Set<IRI> types = NanopubUtils.getTypes(np);
9✔
84
        List<Statement> out = new ArrayList<>();
12✔
85

86
        boolean isSpace = types.contains(GEN.SPACE);
12✔
87
        boolean isHasRole = types.contains(GEN.HAS_ROLE);
12✔
88
        boolean isSpaceMemberRole = types.contains(GEN.SPACE_MEMBER_ROLE);
12✔
89
        boolean isRoleInstantiation = types.contains(GEN.ROLE_INSTANTIATION)
18!
90
                || anyMatch(types, BackcompatRolePredicates.ALL);
18✔
91

92
        if (!isSpace && !isHasRole && !isSpaceMemberRole && !isRoleInstantiation) {
24✔
93
            return Collections.emptyList();
6✔
94
        }
95

96
        if (isSpace) extractSpace(np, ctx, out);
18✔
97
        if (isHasRole) extractHasRole(np, ctx, out);
18✔
98
        if (isSpaceMemberRole) extractSpaceMemberRole(np, ctx, out);
18✔
99
        if (isRoleInstantiation) extractRoleInstantiation(np, ctx, out);
18✔
100

101
        return out;
6✔
102
    }
103

104
    /**
105
     * Emits the {@link SpacesVocab#INVALIDATION} entry for an invalidator nanopub
106
     * whose target has at least one space-relevant type. Caller (the loader's
107
     * invalidation-propagation loop) passes in the types of the invalidated
108
     * nanopub so we can check space-relevance without re-reading the meta repo.
109
     *
110
     * @param thisNp        the invalidator nanopub
111
     * @param invalidatedNp URI of the nanopub being invalidated
112
     * @param targetTypes   types of the invalidated nanopub (from the meta repo)
113
     * @param ctx           extraction context for the invalidator
114
     * @return the invalidation entry statements, or empty if no target type is space-relevant
115
     */
116
    public static List<Statement> extractInvalidation(Nanopub thisNp, IRI invalidatedNp,
117
                                                      Set<IRI> targetTypes, Context ctx) {
118
        if (!isSpaceRelevant(targetTypes)) return Collections.emptyList();
15✔
119
        IRI subject = SpacesVocab.forInvalidation(ctx.artifactCode());
12✔
120
        List<Statement> out = new ArrayList<>();
12✔
121
        out.add(vf.createStatement(subject, RDF.TYPE, SpacesVocab.INVALIDATION, GRAPH));
27✔
122
        out.add(vf.createStatement(subject, SpacesVocab.INVALIDATES, invalidatedNp, GRAPH));
27✔
123
        out.add(vf.createStatement(subject, SpacesVocab.VIA_NANOPUB, thisNp.getUri(), GRAPH));
30✔
124
        addProvenance(subject, ctx, out);
12✔
125
        return out;
6✔
126
    }
127

128
    /** True iff any type in {@code types} is a predefined type or a backwards-compat predicate. */
129
    public static boolean isSpaceRelevant(Set<IRI> types) {
130
        return types.contains(GEN.SPACE)
21✔
131
                || types.contains(GEN.HAS_ROLE)
12✔
132
                || types.contains(GEN.SPACE_MEMBER_ROLE)
12✔
133
                || types.contains(GEN.ROLE_INSTANTIATION)
12✔
134
                || anyMatch(types, BackcompatRolePredicates.ALL);
15✔
135
    }
136

137
    // ---------------- gen:Space ----------------
138

139
    private static void extractSpace(Nanopub np, Context ctx, List<Statement> out) {
140
        // A single gen:Space nanopub may declare multiple Space IRIs, each via its own
141
        // gen:hasRootDefinition triple. We emit one SpaceRef + SpaceDefinition per
142
        // Space IRI. A nanopub missing any hasRootDefinition is accepted as its own
143
        // root for every Space IRI it declares (transition backcompat).
144
        Set<IRI> handled = new LinkedHashSet<>();
12✔
145
        List<IRI> adminAgents = collectAdminAgents(np);
9✔
146

147
        // Rooted case: gen:hasRootDefinition explicitly declared.
148
        for (Statement st : np.getAssertion()) {
33✔
149
            if (!st.getPredicate().equals(GEN.HAS_ROOT_DEFINITION)) continue;
18✔
150
            if (!(st.getSubject() instanceof IRI spaceIri)) continue;
27!
151
            if (!(st.getObject() instanceof IRI rootUri)) continue;
27!
152
            String rootNanopubId = TrustyUriUtils.getArtifactCode(rootUri.stringValue());
12✔
153
            if (rootNanopubId == null || rootNanopubId.isEmpty()) {
15!
154
                log.warn("Ignoring space {}: gen:hasRootDefinition target is not a trusty URI: {}",
×
155
                        spaceIri, rootUri);
156
                continue;
×
157
            }
158
            if (!handled.add(spaceIri)) continue;
12!
159
            emitSpaceEntry(np, ctx, spaceIri, rootUri, rootNanopubId, adminAgents, out);
24✔
160
        }
3✔
161

162
        // Rootless transition case: any Space IRI in the assertion that didn't get a
163
        // hasRootDefinition triple is treated as if it were its own root. Detect by
164
        // looking for triples that reference a Space IRI we haven't handled yet —
165
        // typically via gen:hasAdmin subjects or the rdf:type gen:Space triple on a
166
        // blank-node assertion subject. The common template publishes the Space IRI
167
        // as the subject of at least one triple in the assertion, so we scan for that.
168
        for (Statement st : np.getAssertion()) {
33✔
169
            if (!(st.getSubject() instanceof IRI spaceIri)) continue;
27!
170
            if (handled.contains(spaceIri)) continue;
15✔
171
            // Skip IRIs that clearly aren't Space IRIs (role IRIs embedded in this nanopub).
172
            if (spaceIri.stringValue().startsWith(np.getUri().stringValue())) continue;
21!
173
            // Require at least one structural signal that this is a Space IRI:
174
            // an rdf:type gen:Space, or a gen:hasAdmin triple with this as subject.
175
            if (!looksLikeSpaceIri(np, spaceIri)) continue;
12!
176
            handled.add(spaceIri);
12✔
177
            String rootNanopubId = TrustyUriUtils.getArtifactCode(np.getUri().stringValue());
15✔
178
            if (rootNanopubId == null || rootNanopubId.isEmpty()) continue;
15!
179
            emitSpaceEntry(np, ctx, spaceIri, np.getUri(), rootNanopubId, adminAgents, out);
27✔
180
        }
3✔
181
    }
3✔
182

183
    private static void emitSpaceEntry(Nanopub np, Context ctx, IRI spaceIri, IRI rootUri,
184
                                       String rootNanopubId, List<IRI> adminAgents,
185
                                       List<Statement> out) {
186
        String spaceRef = rootNanopubId + "_" + Utils.createHash(spaceIri);
15✔
187
        IRI refIri = SpacesVocab.forSpaceRef(spaceRef);
9✔
188
        IRI defIri = SpacesVocab.forSpaceDefinition(ctx.artifactCode());
12✔
189

190
        // Aggregate entry: contributor-independent, reinforced on every contribution.
191
        out.add(vf.createStatement(refIri, RDF.TYPE, SpacesVocab.SPACE_REF, GRAPH));
27✔
192
        out.add(vf.createStatement(refIri, SpacesVocab.SPACE_IRI, spaceIri, GRAPH));
27✔
193
        out.add(vf.createStatement(refIri, SpacesVocab.ROOT_NANOPUB, rootUri, GRAPH));
27✔
194

195
        // Per-contributor entry: signer, pubkey, created-at, link back to nanopub.
196
        out.add(vf.createStatement(defIri, RDF.TYPE, SpacesVocab.SPACE_DEFINITION, GRAPH));
27✔
197
        out.add(vf.createStatement(defIri, SpacesVocab.FOR_SPACE_REF, refIri, GRAPH));
27✔
198
        out.add(vf.createStatement(defIri, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
30✔
199
        addProvenance(defIri, ctx, out);
12✔
200

201
        // Trust seed: this is the root nanopub iff rootUri equals the nanopub's own URI.
202
        boolean isOwnRoot = rootUri.equals(np.getUri());
15✔
203
        if (isOwnRoot) {
6✔
204
            for (IRI adminAgent : adminAgents) {
30✔
205
                out.add(vf.createStatement(defIri, SpacesVocab.HAS_ROOT_ADMIN, adminAgent, GRAPH));
27✔
206
            }
3✔
207
        }
208

209
        // gen:RoleInstantiation entry for the admins asserted in this gen:Space nanopub,
210
        // so admins show up in the same SPARQL pattern as ordinary admin instantiations.
211
        if (!adminAgents.isEmpty()) {
9!
212
            IRI riIri = SpacesVocab.forRoleInstantiation(ctx.artifactCode());
12✔
213
            out.add(vf.createStatement(riIri, RDF.TYPE, GEN.ROLE_INSTANTIATION, GRAPH));
27✔
214
            out.add(vf.createStatement(riIri, SpacesVocab.FOR_SPACE, spaceIri, GRAPH));
27✔
215
            out.add(vf.createStatement(riIri, SpacesVocab.INVERSE_PROPERTY, GEN.HAS_ADMIN, GRAPH));
27✔
216
            for (IRI adminAgent : adminAgents) {
30✔
217
                out.add(vf.createStatement(riIri, SpacesVocab.FOR_AGENT, adminAgent, GRAPH));
27✔
218
            }
3✔
219
            out.add(vf.createStatement(riIri, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
30✔
220
            addProvenance(riIri, ctx, out);
12✔
221
        }
222
    }
3✔
223

224
    /**
225
     * Heuristic: does {@code candidate} look like a Space IRI in {@code np}'s assertion,
226
     * independent of any {@code gen:hasRootDefinition} triple? We accept it if the
227
     * assertion contains {@code candidate rdf:type gen:Space} or
228
     * {@code candidate gen:hasAdmin ?x}.
229
     */
230
    private static boolean looksLikeSpaceIri(Nanopub np, IRI candidate) {
231
        for (Statement st : np.getAssertion()) {
33!
232
            if (!candidate.equals(st.getSubject())) continue;
15!
233
            if (st.getPredicate().equals(RDF.TYPE) && GEN.SPACE.equals(st.getObject())) return true;
36!
234
            if (st.getPredicate().equals(GEN.HAS_ADMIN)) return true;
×
235
        }
×
236
        return false;
×
237
    }
238

239
    private static List<IRI> collectAdminAgents(Nanopub np) {
240
        Set<IRI> agents = new LinkedHashSet<>();
12✔
241
        for (Statement st : np.getAssertion()) {
33✔
242
            if (!st.getPredicate().equals(GEN.HAS_ADMIN)) continue;
18✔
243
            if (!(st.getObject() instanceof IRI agent)) continue;
27!
244
            agents.add(agent);
12✔
245
        }
3✔
246
        return new ArrayList<>(agents);
15✔
247
    }
248

249
    // ---------------- gen:hasRole (role attachment) ----------------
250

251
    private static void extractHasRole(Nanopub np, Context ctx, List<Statement> out) {
252
        // A gen:hasRole nanopub asserts <space> gen:hasRole <role>.
253
        for (Statement st : np.getAssertion()) {
33!
254
            if (!st.getPredicate().equals(GEN.HAS_ROLE)) continue;
15!
255
            if (!(st.getSubject() instanceof IRI spaceIri)) continue;
27!
256
            if (!(st.getObject() instanceof IRI roleIri)) continue;
27!
257
            IRI subject = SpacesVocab.forRoleAssignment(ctx.artifactCode());
12✔
258
            out.add(vf.createStatement(subject, RDF.TYPE, GEN.ROLE_ASSIGNMENT, GRAPH));
27✔
259
            out.add(vf.createStatement(subject, SpacesVocab.FOR_SPACE, spaceIri, GRAPH));
27✔
260
            out.add(vf.createStatement(subject, GEN.HAS_ROLE, roleIri, GRAPH));
27✔
261
            out.add(vf.createStatement(subject, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
30✔
262
            addProvenance(subject, ctx, out);
12✔
263
            // One attachment per nanopub — the subject IRI is derived from the nanopub
264
            // artifact code so multiple hasRole triples in the same nanopub would collide.
265
            // If that case shows up in practice, we'll refine the subject-minting scheme.
266
            return;
3✔
267
        }
268
    }
×
269

270
    // ---------------- gen:SpaceMemberRole (role declaration) ----------------
271

272
    private static void extractSpaceMemberRole(Nanopub np, Context ctx, List<Statement> out) {
273
        // The role IRI is embedded in this nanopub, so look for an assertion statement
274
        // of the shape <roleIri> rdf:type gen:SpaceMemberRole where <roleIri> starts
275
        // with the nanopub IRI (valid embedded mint).
276
        IRI roleIri = null;
6✔
277
        for (Statement st : np.getAssertion()) {
33✔
278
            if (!st.getPredicate().equals(RDF.TYPE)) continue;
15!
279
            if (!GEN.SPACE_MEMBER_ROLE.equals(st.getObject())) continue;
18✔
280
            if (!(st.getSubject() instanceof IRI candidate)) continue;
27!
281
            if (!candidate.stringValue().startsWith(np.getUri().stringValue())) continue;
24✔
282
            roleIri = candidate;
6✔
283
            break;
3✔
284
        }
285
        if (roleIri == null) return;
9✔
286

287
        IRI roleType = findRoleTier(np, roleIri);
12✔
288
        List<IRI> regulars = collectRolePredicate(np, roleIri, GEN.HAS_REGULAR_PROPERTY);
15✔
289
        List<IRI> inverses = collectRolePredicate(np, roleIri, GEN.HAS_INVERSE_PROPERTY);
15✔
290

291
        IRI subject = SpacesVocab.forRoleDeclaration(ctx.artifactCode());
12✔
292
        out.add(vf.createStatement(subject, RDF.TYPE, SpacesVocab.ROLE_DECLARATION, GRAPH));
27✔
293
        out.add(vf.createStatement(subject, SpacesVocab.ROLE, roleIri, GRAPH));
27✔
294
        out.add(vf.createStatement(subject, SpacesVocab.HAS_ROLE_TYPE, roleType, GRAPH));
27✔
295
        for (IRI reg : regulars) {
30✔
296
            out.add(vf.createStatement(subject, GEN.HAS_REGULAR_PROPERTY, reg, GRAPH));
27✔
297
        }
3✔
298
        for (IRI inv : inverses) {
30✔
299
            out.add(vf.createStatement(subject, GEN.HAS_INVERSE_PROPERTY, inv, GRAPH));
27✔
300
        }
3✔
301
        out.add(vf.createStatement(subject, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
30✔
302
        if (ctx.createdAt() != null) {
9!
303
            out.add(vf.createStatement(subject, DCTERMS.CREATED, vf.createLiteral(ctx.createdAt()), GRAPH));
36✔
304
        }
305
    }
3✔
306

307
    /**
308
     * Looks for a tier rdf:type ({@code gen:MaintainerRole} / {@code gen:MemberRole} /
309
     * {@code gen:ObserverRole}) on the role IRI in the assertion; defaults to
310
     * {@code gen:ObserverRole} if none is declared.
311
     */
312
    private static IRI findRoleTier(Nanopub np, IRI roleIri) {
313
        for (Statement st : np.getAssertion()) {
33✔
314
            if (!roleIri.equals(st.getSubject())) continue;
15!
315
            if (!st.getPredicate().equals(RDF.TYPE)) continue;
15!
316
            if (!(st.getObject() instanceof IRI type)) continue;
27!
317
            if (GEN.MAINTAINER_ROLE.equals(type) || GEN.MEMBER_ROLE.equals(type)
30!
318
                    || GEN.OBSERVER_ROLE.equals(type)) {
6!
319
                return type;
6✔
320
            }
321
        }
3✔
322
        return GEN.OBSERVER_ROLE;
6✔
323
    }
324

325
    private static List<IRI> collectRolePredicate(Nanopub np, IRI roleIri, IRI predicate) {
326
        List<IRI> out = new ArrayList<>();
12✔
327
        for (Statement st : np.getAssertion()) {
33✔
328
            if (!roleIri.equals(st.getSubject())) continue;
15!
329
            if (!predicate.equals(st.getPredicate())) continue;
18✔
330
            if (!(st.getObject() instanceof IRI obj)) continue;
27!
331
            out.add(obj);
12✔
332
        }
3✔
333
        return out;
6✔
334
    }
335

336
    // ---------------- gen:RoleInstantiation (and backcompat) ----------------
337

338
    private static void extractRoleInstantiation(Nanopub np, Context ctx, List<Statement> out) {
339
        // Find the assignment triple. Directionality (matches the publisher convention
340
        // used by gen:hasRegularProperty / gen:hasInverseProperty in role-definition
341
        // nanopubs):
342
        //   REGULAR: <agent> <predicate> <space>  → npa:regularProperty.
343
        //   INVERSE: <space> <predicate> <agent>  → npa:inverseProperty.
344
        // gen:hasAdmin is hardcoded INVERSE (space-centric: <space> hasAdmin <agent>).
345
        // The 14 backwards-compat predicates are classified in
346
        // {@link BackcompatRolePredicates#DIRECTIONS}. User-defined role predicates from
347
        // gen:SpaceMemberRole nanopubs aren't resolvable here without the role-declaration
348
        // registry; FIXME: the materializer in PR 2 should refine direction for the
349
        // typed-but-unknown-predicate case. For now we emit only triples whose predicate
350
        // we know the direction of.
351
        for (Statement st : np.getAssertion()) {
33!
352
            IRI predicate = st.getPredicate();
9✔
353
            BackcompatRolePredicates.Direction direction = directionFor(predicate);
9✔
354
            if (direction == null) continue;
6!
355
            if (!(st.getSubject() instanceof IRI subjIri)) continue;
27!
356
            if (!(st.getObject() instanceof IRI objIri)) continue;
27!
357

358
            IRI spaceSide;
359
            IRI agentSide;
360
            if (direction == BackcompatRolePredicates.Direction.REGULAR) {
9✔
361
                agentSide = subjIri;
6✔
362
                spaceSide = objIri;
9✔
363
            } else {
364
                spaceSide = subjIri;
6✔
365
                agentSide = objIri;
6✔
366
            }
367

368
            // Deduplicate against the (possibly already emitted) admin instantiation
369
            // from the gen:Space path — a single nanopub can be typed gen:Space AND
370
            // have a gen:hasAdmin triple that the backcompat list also catches. The
371
            // subject IRI is the same (derived from artifact code) and the payload
372
            // would conflict if re-emitted. Skip if we already have a RoleInstantiation
373
            // entry on this subject.
374
            IRI subject = SpacesVocab.forRoleInstantiation(ctx.artifactCode());
12✔
375
            Statement typeSt = vf.createStatement(subject, RDF.TYPE, GEN.ROLE_INSTANTIATION, GRAPH);
21✔
376
            if (out.contains(typeSt)) return;
12!
377

378
            out.add(typeSt);
12✔
379
            out.add(vf.createStatement(subject, SpacesVocab.FOR_SPACE, spaceSide, GRAPH));
27✔
380
            IRI directionPredicate = (direction == BackcompatRolePredicates.Direction.REGULAR)
9✔
381
                    ? SpacesVocab.REGULAR_PROPERTY
6✔
382
                    : SpacesVocab.INVERSE_PROPERTY;
6✔
383
            out.add(vf.createStatement(subject, directionPredicate, predicate, GRAPH));
27✔
384
            out.add(vf.createStatement(subject, SpacesVocab.FOR_AGENT, agentSide, GRAPH));
27✔
385
            out.add(vf.createStatement(subject, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
30✔
386
            addProvenance(subject, ctx, out);
12✔
387
            return;
3✔
388
        }
389
    }
×
390

391
    private static BackcompatRolePredicates.Direction directionFor(IRI predicate) {
392
        if (GEN.HAS_ADMIN.equals(predicate)) return BackcompatRolePredicates.Direction.INVERSE;
12!
393
        return BackcompatRolePredicates.DIRECTIONS.get(predicate);
15✔
394
    }
395

396
    // ---------------- shared helpers ----------------
397

398
    private static void addProvenance(Resource subject, Context ctx, List<Statement> out) {
399
        if (ctx.signedBy() != null) {
9!
400
            out.add(vf.createStatement(subject, NPX.SIGNED_BY, ctx.signedBy(), GRAPH));
30✔
401
        }
402
        if (ctx.pubkeyHash() != null) {
9!
403
            out.add(vf.createStatement(subject, SpacesVocab.PUBKEY_HASH,
27✔
404
                    vf.createLiteral(ctx.pubkeyHash()), GRAPH));
9✔
405
        }
406
        if (ctx.createdAt() != null) {
9!
407
            Literal ts = vf.createLiteral(ctx.createdAt());
15✔
408
            out.add(vf.createStatement(subject, DCTERMS.CREATED, ts, GRAPH));
27✔
409
        }
410
    }
3✔
411

412
    private static boolean anyMatch(Set<IRI> types, Set<IRI> candidates) {
413
        for (IRI c : candidates) {
30✔
414
            if (types.contains(c)) return true;
18✔
415
        }
3✔
416
        return false;
6✔
417
    }
418

419
    // ---------------- load-number stamping ----------------
420

421
    /**
422
     * Stamps {@code <thisNP> npa:hasLoadNumber <N>} on the given nanopub. Intended to
423
     * be called by the loader once per nanopub, in the same transaction as the
424
     * extraction writes. Also bumps {@code npa:thisRepo npa:currentLoadCounter <N>}
425
     * in the admin graph so the materializer's delta cycles know the horizon.
426
     *
427
     * @param npId        nanopub IRI
428
     * @param loadNumber  the load counter value
429
     * @return two statements: load-number stamp + current-load-counter value
430
     */
431
    public static List<Statement> loadCounterStatements(IRI npId, long loadNumber) {
432
        List<Statement> out = new ArrayList<>(2);
15✔
433
        Literal lit = vf.createLiteral(loadNumber);
12✔
434
        out.add(vf.createStatement(npId, NPA.HAS_LOAD_NUMBER, lit, NPA.GRAPH));
27✔
435
        out.add(vf.createStatement(NPA.THIS_REPO, SpacesVocab.CURRENT_LOAD_COUNTER, lit, NPA.GRAPH));
27✔
436
        return out;
6✔
437
    }
438

439
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc