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

knowledgepixels / nanopub-query / 24898989833

24 Apr 2026 03:58PM UTC coverage: 59.078% (-0.4%) from 59.461%
24898989833

Pull #81

github

web-flow
Merge c04bdaa00 into 2324af847
Pull Request #81: feat: per-tier SPARQL UPDATE loops in full build (#62, PR 2b/3)

385 of 740 branches covered (52.03%)

Branch coverage included in aggregate %.

1063 of 1711 relevant lines covered (62.13%)

9.41 hits per line

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

13.96
src/main/java/com/knowledgepixels/query/AuthorityResolver.java
1
package com.knowledgepixels.query;
2

3
import java.util.ArrayList;
4
import java.util.List;
5
import java.util.Optional;
6
import java.util.Set;
7

8
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
9
import org.eclipse.rdf4j.model.IRI;
10
import org.eclipse.rdf4j.model.Statement;
11
import org.eclipse.rdf4j.model.Value;
12
import org.eclipse.rdf4j.model.ValueFactory;
13
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
14
import org.eclipse.rdf4j.model.vocabulary.RDF;
15
import org.eclipse.rdf4j.query.BindingSet;
16
import org.eclipse.rdf4j.query.QueryLanguage;
17
import org.eclipse.rdf4j.query.TupleQueryResult;
18
import org.eclipse.rdf4j.repository.RepositoryConnection;
19
import org.eclipse.rdf4j.repository.RepositoryResult;
20
import org.nanopub.vocabulary.NPA;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import com.knowledgepixels.query.vocabulary.GEN;
25
import com.knowledgepixels.query.vocabulary.NPAT;
26
import com.knowledgepixels.query.vocabulary.SpacesVocab;
27

28
/**
29
 * Drives the space-state materialization pipeline: detects trust-state flips,
30
 * mirrors the approved {@code (agent, pubkey)} rows from the {@code trust} repo
31
 * into a fresh {@code npass:<T>_<M>} graph in the {@code spaces} repo, runs the
32
 * per-tier validation loops (stubbed in PR 2a), flips the
33
 * {@code npa:hasCurrentSpaceState} pointer, and drops the previous graph. Also
34
 * cleans up orphan {@code npass:*} graphs on startup.
35
 *
36
 * <p>See {@code doc/plan-space-repositories.md} — this implements the "Full
37
 * build" procedure plus pointer management and the mirror step. Per-tier SPARQL
38
 * UPDATE loops, the incremental cycle, and the periodic-rebuild flag follow in
39
 * PRs 2b and 2c.
40
 */
41
public final class AuthorityResolver {
42

43
    private static final Logger log = LoggerFactory.getLogger(AuthorityResolver.class);
9✔
44

45
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
46

47
    private static final String SPACES_REPO = "spaces";
48
    private static final String TRUST_REPO = "trust";
49

50
    /** NPA constants pulled in locally (trust-side). */
51
    private static final IRI NPA_HAS_CURRENT_TRUST_STATE =
9✔
52
            vf.createIRI(NPA.NAMESPACE, "hasCurrentTrustState");
6✔
53
    private static final IRI NPA_ACCOUNT_STATE = vf.createIRI(NPA.NAMESPACE, "AccountState");
15✔
54
    private static final IRI NPA_AGENT = vf.createIRI(NPA.NAMESPACE, "agent");
15✔
55
    private static final IRI NPA_PUBKEY = vf.createIRI(NPA.NAMESPACE, "pubkey");
15✔
56
    private static final IRI NPA_TRUST_STATUS = vf.createIRI(NPA.NAMESPACE, "trustStatus");
15✔
57
    private static final IRI NPA_LOADED = vf.createIRI(NPA.NAMESPACE, "loaded");
15✔
58
    private static final IRI NPA_TO_LOAD = vf.createIRI(NPA.NAMESPACE, "toLoad");
15✔
59

60
    /**
61
     * Trust-approved set: rows with one of these {@code npa:trustStatus} values
62
     * are mirrored into the space-state graph. Per
63
     * {@code doc/design-trust-state-repos.md}, these are the two "authority-
64
     * approving" statuses; {@code npa:contested}, {@code npa:skipped}, and the
65
     * transient statuses are distinct values of the same predicate and are
66
     * excluded automatically by this positive-list filter.
67
     */
68
    private static final Set<IRI> APPROVED_SET = Set.of(NPA_LOADED, NPA_TO_LOAD);
15✔
69

70
    private static AuthorityResolver instance;
71

72
    /** Returns the singleton. */
73
    public static synchronized AuthorityResolver get() {
74
        if (instance == null) {
6✔
75
            instance = new AuthorityResolver();
12✔
76
        }
77
        return instance;
6✔
78
    }
79

80
    private AuthorityResolver() {
81
    }
82

83
    // ---------------- Public entry points ----------------
84

85
    /**
86
     * Poll entry point: checks the current trust-state hash against the active
87
     * space-state graph's hash component; if they differ (or no space-state graph
88
     * exists yet), runs a full build. Safe to call repeatedly on a schedule —
89
     * when the hashes match, it's a no-op. Gated by
90
     * {@link FeatureFlags#spacesEnabled()}.
91
     */
92
    public void tick() {
93
        if (!FeatureFlags.spacesEnabled()) return;
6!
94
        String trustStateHash = TrustStateRegistry.get().getCurrentHash().orElse(null);
18✔
95
        if (trustStateHash == null) {
6!
96
            log.debug("AuthorityResolver.tick: no current trust state yet — skipping");
9✔
97
            return;
3✔
98
        }
99
        String currentGraphName = getCurrentSpaceStateGraphLocalName();
×
100
        if (currentGraphName != null && currentGraphName.startsWith(trustStateHash + "_")) {
×
101
            log.debug("AuthorityResolver.tick: already on trust state {}", abbrev(trustStateHash));
×
102
            return;
×
103
        }
104
        log.info("AuthorityResolver.tick: trust-state flip detected (now {}); running full build",
×
105
                abbrev(trustStateHash));
×
106
        runFullBuild(trustStateHash);
×
107
    }
×
108

109
    /**
110
     * Startup cleanup: drop any {@code npass:*} graph that the
111
     * {@code npa:hasCurrentSpaceState} pointer isn't pointing at. Orphans come
112
     * from crashes mid-build. Safe to call at any time; idempotent.
113
     */
114
    public void cleanOrphans() {
115
        if (!FeatureFlags.spacesEnabled()) return;
×
116
        IRI current = getCurrentSpaceStateGraph();
×
117
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
118
            int dropped = 0;
×
119
            try (RepositoryResult<org.eclipse.rdf4j.model.Resource> ctxs = conn.getContextIDs()) {
×
120
                List<IRI> toDrop = new ArrayList<>();
×
121
                while (ctxs.hasNext()) {
×
122
                    org.eclipse.rdf4j.model.Resource ctx = ctxs.next();
×
123
                    if (!(ctx instanceof IRI iri)) continue;
×
124
                    if (!iri.stringValue().startsWith(SpacesVocab.NPASS_NAMESPACE)) continue;
×
125
                    if (iri.equals(current)) continue;
×
126
                    toDrop.add(iri);
×
127
                }
×
128
                for (IRI iri : toDrop) {
×
129
                    conn.begin(IsolationLevels.SERIALIZABLE);
×
130
                    conn.clear(iri);
×
131
                    conn.commit();
×
132
                    dropped++;
×
133
                    log.info("AuthorityResolver.cleanOrphans: dropped orphan graph {}", iri);
×
134
                }
×
135
            }
136
            if (dropped == 0) {
×
137
                log.debug("AuthorityResolver.cleanOrphans: no orphan space-state graphs");
×
138
            }
139
        } catch (Exception ex) {
×
140
            log.info("AuthorityResolver.cleanOrphans: failed: {}", ex.toString());
×
141
        }
×
142
    }
×
143

144
    // ---------------- Full build ----------------
145

146
    /**
147
     * Mutex-protected full build of the space-state graph for the given trust
148
     * state. Captures {@code M = currentLoadCounter}, mirrors trust-approved
149
     * rows, (PR 2b: runs per-tier UPDATE loops from scratch), stamps
150
     * {@code processedUpTo = M}, flips the pointer, drops the previous graph.
151
     */
152
    synchronized void runFullBuild(String trustStateHash) {
153
        long loadCounter = getCurrentLoadCounter();
×
154
        IRI newGraph = SpacesVocab.forSpaceState(trustStateHash, loadCounter);
×
155
        IRI oldGraph = getCurrentSpaceStateGraph();
×
156
        if (newGraph.equals(oldGraph)) {
×
157
            log.debug("AuthorityResolver.runFullBuild: already current at {}", newGraph);
×
158
            return;
×
159
        }
160

161
        // 1. Mirror trust-approved rows into the new graph.
162
        int mirrored = mirrorTrustState(trustStateHash, newGraph);
×
163

164
        // 2. Per-tier UPDATE loops (from scratch: lastProcessed = -1 so the
165
        //    delta filter FILTER(?ln > ?lastProcessed) includes everything).
166
        TierCounts counts = runAllTierLoops(newGraph, -1);
×
167

168
        // 3. Stamp processedUpTo inside the new graph.
169
        writeProcessedUpTo(newGraph, loadCounter);
×
170

171
        // 4. Flip the current-space-state pointer.
172
        flipPointer(newGraph);
×
173

174
        // 5. Drop the old graph if one existed.
175
        if (oldGraph != null) {
×
176
            dropGraph(oldGraph);
×
177
        }
178

179
        log.info("AuthorityResolver: full build complete — graph={} mirrored={} rows loadCounter={} "
×
180
                        + "tiers: admin={} attachment={} maintainer={} member={} observer={}",
181
                newGraph, mirrored, loadCounter,
×
182
                counts.admin, counts.attachment, counts.maintainer, counts.member, counts.observer);
×
183
    }
×
184

185
    // ---------------- Tier UPDATE loops ----------------
186

187
    /** Per-tier INSERT counts (for logging/metrics). */
188
    static final class TierCounts {
×
189
        int admin;
190
        int attachment;
191
        int maintainer;
192
        int member;
193
        int observer;
194
    }
195

196
    /**
197
     * Runs the five tier loops in order: admin → {@code gen:hasRole} attachment
198
     * validation → maintainer → member → observer. Each loop iterates a SPARQL
199
     * INSERT to fixed point (no new triples added). Returns per-tier counts.
200
     *
201
     * @param graph         target space-state graph
202
     * @param lastProcessed load-number horizon; use {@code -1} for full build
203
     */
204
    TierCounts runAllTierLoops(IRI graph, long lastProcessed) {
205
        TierCounts c = new TierCounts();
×
206
        c.admin = runTierLoop(graph, adminTierUpdate(graph, lastProcessed));
×
207
        c.attachment = runTierLoop(graph, attachmentValidationUpdate(graph, lastProcessed));
×
208
        c.maintainer = runTierLoop(graph, nonAdminTierUpdate(graph, lastProcessed,
×
209
                GEN.MAINTAINER_ROLE, PUBLISHER_IS_ADMIN));
210
        c.member = runTierLoop(graph, nonAdminTierUpdate(graph, lastProcessed,
×
211
                GEN.MEMBER_ROLE, PUBLISHER_IS_ADMIN_OR_MAINTAINER));
212
        c.observer = runTierLoop(graph, nonAdminTierUpdate(graph, lastProcessed,
×
213
                GEN.OBSERVER_ROLE, PUBLISHER_IS_SELF_OR_TIERED));
214
        return c;
×
215
    }
216

217
    /**
218
     * Runs a single tier's INSERT to fixed point. Counts rows by probing
219
     * graph size before/after each INSERT; stops when the size doesn't change.
220
     *
221
     * @return total number of triples inserted by this tier across all iterations
222
     */
223
    int runTierLoop(IRI graph, String sparqlUpdate) {
224
        int total = 0;
×
225
        long before = graphSize(graph);
×
226
        while (true) {
227
            try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
228
                conn.begin(IsolationLevels.SERIALIZABLE);
×
229
                conn.prepareUpdate(QueryLanguage.SPARQL, sparqlUpdate).execute();
×
230
                conn.commit();
×
231
            }
232
            long after = graphSize(graph);
×
233
            long added = after - before;
×
234
            if (added <= 0) break;
×
235
            total += added;
×
236
            before = after;
×
237
        }
×
238
        return total;
×
239
    }
240

241
    private long graphSize(IRI graph) {
242
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
243
            return conn.size(graph);
×
244
        }
245
    }
246

247
    // ---------------- SPARQL templates ----------------
248

249
    /** Reusable invalidation filter on a bound nanopub-IRI variable. */
250
    private static String invalidationFilter(String npVar) {
251
        return "FILTER NOT EXISTS { ?_inv_" + npVar + " a <" + SpacesVocab.INVALIDATION + "> ; "
24✔
252
                + "<" + SpacesVocab.INVALIDATES + "> " + npVar + " . }";
253
    }
254

255
    /**
256
     * Admin tier: seed from {@code npadef:...hasRootAdmin} (trusted by construction)
257
     * plus closed-over admin grants; insert any {@code gen:RoleInstantiation} with
258
     * {@code npa:regularProperty gen:hasAdmin} whose publisher (resolved via mirrored
259
     * trust-approved AccountState) is already in the admin set.
260
     */
261
    static String adminTierUpdate(IRI graph, long lastProcessed) {
262
        return """
69✔
263
                PREFIX npa:  <%1$s>
264
                PREFIX gen:  <%2$s>
265
                INSERT { GRAPH <%3$s> {
266
                  ?ri a gen:RoleInstantiation ;
267
                      npa:forSpace ?space ;
268
                      npa:regularProperty gen:hasAdmin ;
269
                      npa:forAgent ?agent ;
270
                      npa:viaNanopub ?np .
271
                } }
272
                WHERE {
273
                  GRAPH <%4$s> {
274
                    ?ri a gen:RoleInstantiation ;
275
                        npa:forSpace        ?space ;
276
                        npa:regularProperty gen:hasAdmin ;
277
                        npa:forAgent        ?agent ;
278
                        npa:pubkeyHash      ?pkh ;
279
                        npa:viaNanopub      ?np .
280
                    ?np npa:hasLoadNumber ?ln .
281
                    FILTER (?ln > %5$d)
282
                    %6$s
283
                  }
284
                  GRAPH <%3$s> {
285
                    ?acct a npa:AccountState ;
286
                          npa:agent  ?publisher ;
287
                          npa:pubkey ?pkh .
288
                    {
289
                      # Seed: root-admin in a non-invalidated SpaceDefinition for this space
290
                      ?def a npa:SpaceDefinition ;
291
                           npa:forSpaceRef ?spaceRef ;
292
                           npa:hasRootAdmin ?publisher ;
293
                           npa:viaNanopub   ?defNp .
294
                      ?spaceRef npa:spaceIri ?space .
295
                    }
296
                    UNION
297
                    {
298
                      # Closed-over: an existing admin instantiation for this space
299
                      ?prev a gen:RoleInstantiation ;
300
                            npa:forSpace ?space ;
301
                            npa:regularProperty gen:hasAdmin ;
302
                            npa:forAgent ?publisher .
303
                    }
304
                  }
305
                  FILTER NOT EXISTS { GRAPH <%3$s> {
306
                    ?existing a gen:RoleInstantiation ;
307
                              npa:forSpace ?space ;
308
                              npa:forAgent ?agent ;
309
                              npa:regularProperty gen:hasAdmin .
310
                  } }
311
                  # Invalidation filter on the SpaceDefinition (seed branch) is only
312
                  # checked if that branch matched. Since ?defNp is unbound in the
313
                  # closed-over branch we apply the filter inside the WHERE's OPTIONAL
314
                  # pattern via a nested FILTER — see adminSeedInvalidationFilter
315
                  # below.
316
                }
317
                """.formatted(
3✔
318
                NPA.NAMESPACE,
319
                GEN.NAMESPACE,
320
                graph,
321
                SpacesVocab.SPACES_GRAPH,
322
                lastProcessed,
15✔
323
                invalidationFilter("?np"));
6✔
324
    }
325

326
    /**
327
     * {@code gen:hasRole} attachment validation: an attachment is validated iff its
328
     * publisher is already a validated admin of the target space. Adds
329
     * {@code gen:RoleAssignment} rows to the space-state graph.
330
     */
331
    static String attachmentValidationUpdate(IRI graph, long lastProcessed) {
332
        return """
69✔
333
                PREFIX npa:  <%1$s>
334
                PREFIX gen:  <%2$s>
335
                INSERT { GRAPH <%3$s> {
336
                  ?ra a gen:RoleAssignment ;
337
                      npa:forSpace ?space ;
338
                      gen:hasRole  ?role ;
339
                      npa:viaNanopub ?np .
340
                } }
341
                WHERE {
342
                  GRAPH <%4$s> {
343
                    ?ra a gen:RoleAssignment ;
344
                        npa:forSpace ?space ;
345
                        gen:hasRole  ?role ;
346
                        npa:pubkeyHash ?pkh ;
347
                        npa:viaNanopub ?np .
348
                    ?np npa:hasLoadNumber ?ln .
349
                    FILTER (?ln > %5$d)
350
                    %6$s
351
                  }
352
                  GRAPH <%3$s> {
353
                    ?acct a npa:AccountState ;
354
                          npa:agent  ?publisher ;
355
                          npa:pubkey ?pkh .
356
                    ?adminRI a gen:RoleInstantiation ;
357
                             npa:forSpace ?space ;
358
                             npa:regularProperty gen:hasAdmin ;
359
                             npa:forAgent ?publisher .
360
                  }
361
                  FILTER NOT EXISTS { GRAPH <%3$s> {
362
                    ?existing a gen:RoleAssignment ;
363
                              npa:forSpace ?space ;
364
                              gen:hasRole  ?role .
365
                  } }
366
                }
367
                """.formatted(
3✔
368
                NPA.NAMESPACE,
369
                GEN.NAMESPACE,
370
                graph,
371
                SpacesVocab.SPACES_GRAPH,
372
                lastProcessed,
15✔
373
                invalidationFilter("?np"));
6✔
374
    }
375

376
    /** Non-admin tier publisher constraints (inserted as a SPARQL sub-pattern). */
377
    static final String PUBLISHER_IS_ADMIN = """
378
            ?adminRI a gen:RoleInstantiation ;
379
                     npa:forSpace ?space ;
380
                     npa:regularProperty gen:hasAdmin ;
381
                     npa:forAgent ?publisher .
382
            """;
383

384
    static final String PUBLISHER_IS_ADMIN_OR_MAINTAINER = """
385
            {
386
              ?adminRI a gen:RoleInstantiation ;
387
                       npa:forSpace ?space ;
388
                       npa:regularProperty gen:hasAdmin ;
389
                       npa:forAgent ?publisher .
390
            }
391
            UNION
392
            {
393
              ?maintRI a gen:RoleInstantiation ;
394
                       npa:forSpace ?space ;
395
                       npa:forAgent ?publisher .
396
              ?rdM a npa:RoleDeclaration ;
397
                   npa:hasRoleType gen:MaintainerRole .
398
              { ?maintRI npa:regularProperty ?predM . ?rdM gen:hasRegularProperty ?predM . }
399
              UNION
400
              { ?maintRI npa:inverseProperty ?predM . ?rdM gen:hasInverseProperty ?predM . }
401
            }
402
            """;
403

404
    /**
405
     * Observer self-evidence: publisher is admin, maintainer, member, OR the
406
     * mirrored trust row confirms that the signing pubkey maps to the assignee
407
     * itself (i.e. the assignee is the publisher).
408
     */
409
    static final String PUBLISHER_IS_SELF_OR_TIERED = """
410
            {
411
              ?adminRI a gen:RoleInstantiation ;
412
                       npa:forSpace ?space ;
413
                       npa:regularProperty gen:hasAdmin ;
414
                       npa:forAgent ?publisher .
415
            }
416
            UNION
417
            {
418
              ?maintRI a gen:RoleInstantiation ;
419
                       npa:forSpace ?space ;
420
                       npa:forAgent ?publisher .
421
              ?rdM a npa:RoleDeclaration ;
422
                   npa:hasRoleType gen:MaintainerRole .
423
              { ?maintRI npa:regularProperty ?predM . ?rdM gen:hasRegularProperty ?predM . }
424
              UNION
425
              { ?maintRI npa:inverseProperty ?predM . ?rdM gen:hasInverseProperty ?predM . }
426
            }
427
            UNION
428
            {
429
              ?memRI a gen:RoleInstantiation ;
430
                     npa:forSpace ?space ;
431
                     npa:forAgent ?publisher .
432
              ?rdMem a npa:RoleDeclaration ;
433
                     npa:hasRoleType gen:MemberRole .
434
              { ?memRI npa:regularProperty ?predMem . ?rdMem gen:hasRegularProperty ?predMem . }
435
              UNION
436
              { ?memRI npa:inverseProperty ?predMem . ?rdMem gen:hasInverseProperty ?predMem . }
437
            }
438
            UNION
439
            {
440
              # Self-evidence: publisher pubkey maps to the assignee.
441
              FILTER (?publisher = ?agent)
442
            }
443
            """;
444

445
    /**
446
     * Maintainer / Member / Observer tier INSERT. Same shape: find an instantiation
447
     * whose predicate matches a RoleDeclaration of the given tier attached to the
448
     * target space, and whose publisher passes the tier-specific constraint.
449
     */
450
    static String nonAdminTierUpdate(IRI graph, long lastProcessed,
451
                                     IRI tierClass, String publisherConstraint) {
452
        return """
69✔
453
                PREFIX npa:  <%1$s>
454
                PREFIX gen:  <%2$s>
455
                INSERT { GRAPH <%3$s> {
456
                  ?ri a gen:RoleInstantiation ;
457
                      npa:forSpace ?space ;
458
                      npa:forAgent ?agent ;
459
                      npa:viaNanopub ?np .
460
                } }
461
                WHERE {
462
                  GRAPH <%4$s> {
463
                    ?ri a gen:RoleInstantiation ;
464
                        npa:forSpace ?space ;
465
                        npa:forAgent ?agent ;
466
                        npa:pubkeyHash ?pkh ;
467
                        npa:viaNanopub ?np .
468
                    { ?ri npa:regularProperty ?pred . }
469
                    UNION
470
                    { ?ri npa:inverseProperty ?pred . }
471
                    ?np npa:hasLoadNumber ?ln .
472
                    FILTER (?ln > %5$d)
473
                    %6$s
474
                    # Predicate maps to a RoleDeclaration of this tier (not invalidated).
475
                    ?rd a npa:RoleDeclaration ;
476
                        npa:role ?role ;
477
                        npa:hasRoleType <%7$s> ;
478
                        npa:viaNanopub ?rdNp .
479
                    { ?rd gen:hasRegularProperty ?pred . }
480
                    UNION
481
                    { ?rd gen:hasInverseProperty ?pred . }
482
                    %8$s
483
                  }
484
                  GRAPH <%3$s> {
485
                    # Role must be admin-attached to the target space.
486
                    ?ra a gen:RoleAssignment ;
487
                        npa:forSpace ?space ;
488
                        gen:hasRole  ?role .
489
                    # Publisher's agent from the mirrored trust-approved set.
490
                    ?acct a npa:AccountState ;
491
                          npa:agent ?publisher ;
492
                          npa:pubkey ?pkh .
493
                    %9$s
494
                  }
495
                  FILTER NOT EXISTS { GRAPH <%3$s> {
496
                    ?existing a gen:RoleInstantiation ;
497
                              npa:forSpace ?space ;
498
                              npa:forAgent ?agent ;
499
                              npa:viaNanopub ?np .
500
                  } }
501
                }
502
                """.formatted(
3✔
503
                NPA.NAMESPACE,
504
                GEN.NAMESPACE,
505
                graph,
506
                SpacesVocab.SPACES_GRAPH,
507
                lastProcessed,
15✔
508
                invalidationFilter("?np"),
27✔
509
                tierClass,
510
                invalidationFilter("?rdNp"),
18✔
511
                publisherConstraint);
512
    }
513

514
    /**
515
     * Copies trust-approved {@code npa:AccountState} rows from {@code npat:<T>}
516
     * in the {@code trust} repo into {@code newGraph} in the {@code spaces} repo,
517
     * inside one spaces-side serializable transaction.
518
     *
519
     * @return number of rows mirrored (useful for metrics / logging)
520
     */
521
    int mirrorTrustState(String trustStateHash, IRI newGraph) {
522
        IRI trustStateIri = NPAT.forHash(trustStateHash);
×
523
        int count = 0;
×
524
        try (RepositoryConnection trustConn = TripleStore.get().getRepoConnection(TRUST_REPO);
×
525
             RepositoryConnection spacesConn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
526
            trustConn.begin(IsolationLevels.READ_COMMITTED);
×
527
            spacesConn.begin(IsolationLevels.SERIALIZABLE);
×
528
            // Walk rdf:type triples in the trust state's graph; for each AccountState,
529
            // check status and copy the approved ones verbatim (minus status-specific
530
            // detail triples, which we don't need for validation).
531
            try (RepositoryResult<Statement> typeRows = trustConn.getStatements(
×
532
                    null, RDF.TYPE, NPA_ACCOUNT_STATE, trustStateIri)) {
533
                while (typeRows.hasNext()) {
×
534
                    Statement st = typeRows.next();
×
535
                    if (!(st.getSubject() instanceof IRI accountStateIri)) continue;
×
536
                    Value status = trustConn.getStatements(accountStateIri, NPA_TRUST_STATUS, null, trustStateIri)
×
537
                            .stream().findFirst().map(Statement::getObject).orElse(null);
×
538
                    if (!(status instanceof IRI statusIri) || !APPROVED_SET.contains(statusIri)) continue;
×
539
                    Value agent = trustConn.getStatements(accountStateIri, NPA_AGENT, null, trustStateIri)
×
540
                            .stream().findFirst().map(Statement::getObject).orElse(null);
×
541
                    Value pubkey = trustConn.getStatements(accountStateIri, NPA_PUBKEY, null, trustStateIri)
×
542
                            .stream().findFirst().map(Statement::getObject).orElse(null);
×
543
                    if (agent == null || pubkey == null) {
×
544
                        log.warn("AuthorityResolver.mirror: account {} missing agent or pubkey; skipping",
×
545
                                accountStateIri);
546
                        continue;
×
547
                    }
548
                    spacesConn.add(accountStateIri, RDF.TYPE, NPA_ACCOUNT_STATE, newGraph);
×
549
                    spacesConn.add(accountStateIri, NPA_AGENT, agent, newGraph);
×
550
                    spacesConn.add(accountStateIri, NPA_PUBKEY, pubkey, newGraph);
×
551
                    spacesConn.add(accountStateIri, NPA_TRUST_STATUS, statusIri, newGraph);
×
552
                    count++;
×
553
                }
×
554
            }
555
            spacesConn.commit();
×
556
            trustConn.commit();
×
557
        }
558
        return count;
×
559
    }
560

561
    // ---------------- Pointer + counter helpers ----------------
562

563
    /**
564
     * Reads the current {@code npa:hasCurrentSpaceState} pointer from the
565
     * {@code npa:graph} admin graph of the {@code spaces} repo. Returns
566
     * {@code null} if no pointer exists yet.
567
     */
568
    IRI getCurrentSpaceStateGraph() {
569
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
570
            Value v = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO,
×
571
                    SpacesVocab.HAS_CURRENT_SPACE_STATE);
572
            return (v instanceof IRI iri) ? iri : null;
×
573
        } catch (Exception ex) {
×
574
            log.warn("AuthorityResolver: failed to read hasCurrentSpaceState pointer: {}", ex.toString());
×
575
            return null;
×
576
        }
577
    }
578

579
    /** Convenience: local-name of the current space-state graph IRI. */
580
    private String getCurrentSpaceStateGraphLocalName() {
581
        IRI iri = getCurrentSpaceStateGraph();
×
582
        if (iri == null) return null;
×
583
        String s = iri.stringValue();
×
584
        if (!s.startsWith(SpacesVocab.NPASS_NAMESPACE)) return null;
×
585
        return s.substring(SpacesVocab.NPASS_NAMESPACE.length());
×
586
    }
587

588
    long getCurrentLoadCounter() {
589
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
590
            Value v = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO,
×
591
                    SpacesVocab.CURRENT_LOAD_COUNTER);
592
            if (v == null) return 0;
×
593
            try {
594
                return Long.parseLong(v.stringValue());
×
595
            } catch (NumberFormatException ex) {
×
596
                log.warn("AuthorityResolver: non-numeric currentLoadCounter: {}", v);
×
597
                return 0;
×
598
            }
599
        } catch (Exception ex) {
×
600
            log.warn("AuthorityResolver: failed to read currentLoadCounter: {}", ex.toString());
×
601
            return 0;
×
602
        }
603
    }
604

605
    /**
606
     * Atomic pointer flip: a single SPARQL {@code DELETE … INSERT … WHERE}
607
     * replaces the old pointer with the new one in one statement, so readers
608
     * never see a zero-pointer window.
609
     */
610
    void flipPointer(IRI newGraph) {
611
        String update = String.format("""
×
612
                DELETE { GRAPH <%s> { <%s> <%s> ?old } }
613
                INSERT { GRAPH <%s> { <%s> <%s> <%s> } }
614
                WHERE  { OPTIONAL { GRAPH <%s> { <%s> <%s> ?old } } }
615
                """,
616
                NPA.GRAPH, NPA.THIS_REPO, SpacesVocab.HAS_CURRENT_SPACE_STATE,
617
                NPA.GRAPH, NPA.THIS_REPO, SpacesVocab.HAS_CURRENT_SPACE_STATE, newGraph,
618
                NPA.GRAPH, NPA.THIS_REPO, SpacesVocab.HAS_CURRENT_SPACE_STATE);
619
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
620
            conn.begin(IsolationLevels.SERIALIZABLE);
×
621
            conn.prepareUpdate(QueryLanguage.SPARQL, update).execute();
×
622
            conn.commit();
×
623
        }
624
    }
×
625

626
    void writeProcessedUpTo(IRI graph, long loadCounter) {
627
        String update = String.format("""
×
628
                DELETE { GRAPH <%s> { <%s> <%s> ?old } }
629
                INSERT { GRAPH <%s> { <%s> <%s> "%d"^^<http://www.w3.org/2001/XMLSchema#long> } }
630
                WHERE  { OPTIONAL { GRAPH <%s> { <%s> <%s> ?old } } }
631
                """,
632
                graph, graph, SpacesVocab.PROCESSED_UP_TO,
633
                graph, graph, SpacesVocab.PROCESSED_UP_TO, loadCounter,
×
634
                graph, graph, SpacesVocab.PROCESSED_UP_TO);
635
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
636
            conn.begin(IsolationLevels.SERIALIZABLE);
×
637
            conn.prepareUpdate(QueryLanguage.SPARQL, update).execute();
×
638
            conn.commit();
×
639
        }
640
    }
×
641

642
    /**
643
     * Reads {@code processedUpTo} from the given space-state graph.
644
     * Returns {@code -1} if absent (graph not fully built yet).
645
     */
646
    long readProcessedUpTo(IRI graph) {
647
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
648
            String query = String.format(
×
649
                    "SELECT ?n WHERE { GRAPH <%s> { <%s> <%s> ?n } }",
650
                    graph, graph, SpacesVocab.PROCESSED_UP_TO);
651
            try (TupleQueryResult r = conn.prepareTupleQuery(QueryLanguage.SPARQL, query).evaluate()) {
×
652
                if (!r.hasNext()) return -1;
×
653
                BindingSet b = r.next();
×
654
                return Long.parseLong(b.getBinding("n").getValue().stringValue());
×
655
            }
×
656
        } catch (Exception ex) {
×
657
            log.warn("AuthorityResolver: failed to read processedUpTo for {}: {}", graph, ex.toString());
×
658
            return -1;
×
659
        }
660
    }
661

662
    void dropGraph(IRI graph) {
663
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(SPACES_REPO)) {
×
664
            conn.begin(IsolationLevels.SERIALIZABLE);
×
665
            conn.clear(graph);
×
666
            conn.commit();
×
667
            log.info("AuthorityResolver: dropped old space-state graph {}", graph);
×
668
        }
669
    }
×
670

671
    // ---------------- Trust-repo pointer lookup (used by TrustStateRegistry's bootstrap) ----------------
672

673
    /**
674
     * Queries the {@code trust} repo directly for the current trust-state hash.
675
     * Prefer {@link TrustStateRegistry#getCurrentHash()} in normal operation —
676
     * this helper exists for tests and diagnostics.
677
     *
678
     * @return the current trust-state hash, or empty if none is set
679
     */
680
    Optional<String> readTrustRepoCurrentHash() {
681
        try (RepositoryConnection conn = TripleStore.get().getRepoConnection(TRUST_REPO)) {
×
682
            Value v = Utils.getObjectForPattern(conn, NPA.GRAPH, NPA.THIS_REPO,
×
683
                    NPA_HAS_CURRENT_TRUST_STATE);
684
            if (!(v instanceof IRI iri)) return Optional.empty();
×
685
            String s = iri.stringValue();
×
686
            if (!s.startsWith(NPAT.NAMESPACE)) return Optional.empty();
×
687
            return Optional.of(s.substring(NPAT.NAMESPACE.length()));
×
688
        } catch (Exception ex) {
×
689
            log.warn("AuthorityResolver: failed to read trust-repo current pointer: {}", ex.toString());
×
690
            return Optional.empty();
×
691
        }
692
    }
693

694
    private static String abbrev(String hash) {
695
        return hash.length() > 12 ? hash.substring(0, 12) + "…" : hash;
×
696
    }
697

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

© 2026 Coveralls, Inc