Skip to content

fix(spaces): resolve custom role-predicate direction+tier from the role declaration#124

Merged
tkuhn merged 1 commit into
mainfrom
fix/custom-role-predicate-resolution
Jun 23, 2026
Merged

fix(spaces): resolve custom role-predicate direction+tier from the role declaration#124
tkuhn merged 1 commit into
mainfrom
fix/custom-role-predicate-resolution

Conversation

@tkuhn

@tkuhn tkuhn commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Problem

A user→space role assignment using a custom role predicate (e.g. gen:hasMaintainer, declared via a gen:SpaceMemberRole nanopub's gen:hasInverseProperty) was silently dropped at extraction. SpacesExtractor.extractRoleInstantiation only emitted a gen:RoleInstantiation for predicates whose direction it knows — gen:hasAdmin plus the BackcompatRolePredicates set. gen:hasMaintainer/gen:hasMember are in neither, so an explicitly gen:RoleInstantiation-typed <space> hasMaintainer <agent> binding never reached npa:spacesGraph, the maintainer tier never materialized, and Maintainer-tier users never showed up in a space's about tab — even though the role declaration already carried everything needed to resolve the binding.

This is the deferred FIXME in extractRoleInstantiation: "the materializer in PR 2 should refine direction for the typed-but-unknown-predicate case."

Fix

Defer direction inference from extraction-time (per-nanopub, can't see the declaration) to materialization-time (where the role-declaration registry is available):

  • Extractor — for an explicitly gen:RoleInstantiation-typed nanopub whose predicate isn't classifiable, emit a neutral binding (npa:rolePredicate + raw npa:bindingSubject/npa:bindingObject, predicate-discriminated subject) instead of dropping it. The known-predicate path is unchanged. Gated on the explicit type so incidental IRI-valued triples don't mint inert rows.
  • MaterializernonAdminTierUpdate's direction-pairing UNION gains two branches that join npa:rolePredicate against the tier-pinned npa:RoleDeclaration attached to the space; the matched gen:hasInverseProperty/gen:hasRegularProperty fixes both the direction and which raw endpoint is the space vs. agent. The INSERT is untouched, so the materialized state row is identical to a pre-classified grant (carries npa:inverseProperty/npa:regularProperty) — the read side needs no special case. Applies uniformly to maintainer/member/observer.
  • New SpacesVocab terms: npa:rolePredicate, npa:bindingSubject, npa:bindingObject.

No hardcoding of hasMaintainer; any future user-defined role predicate now works via its declaration. The hardcoded directionFor set remains a fallback for predicates without a declaration.

Tests

  • SpacesExtractorTest: emits the neutral binding for an explicitly-typed custom predicate; gates it off when the type is absent.
  • AuthorityResolverTierIsolationTest: end-to-end resolution of both INVERSE and REGULAR custom predicates into the correct ref, carrying the resolved direction property.
  • AuthorityResolverTest: nonAdminTierUpdate SPARQL resolves neutral bindings.
  • Full suite: 275 passing.

Live verification

Verified against localhost:9393 after a fresh full re-sync: the published binding RAuAs_L_… (<biodiversity> hasMaintainer <orcid>) now extracts as a neutral RI, materializes into the current state graph with the resolved npa:inverseProperty gen:hasMaintainer, and the about-tab read join returns the Maintainer-tier users (3 maintainers across multiple bindings). Admin tier unaffected.

Design doc updated.

🤖 Generated with Claude Code

…le declaration

A user-defined role predicate (e.g. gen:hasMaintainer, declared via a
gen:SpaceMemberRole nanopub's gen:hasInverseProperty) was dropped at extraction:
SpacesExtractor only emitted a RoleInstantiation for gen:hasAdmin and the
backcompat predicates, whose direction it knows. So an explicitly
gen:RoleInstantiation-typed "<space> hasMaintainer <agent>" binding never reached
npa:spacesGraph, and the maintainer tier never materialized — even though the
role declaration already carried everything needed to resolve it.

Defer direction inference from extraction-time to materialization-time, where the
role-declaration registry is available (the deferred FIXME in extractRoleInstantiation):

- Extractor: for an explicitly gen:RoleInstantiation-typed nanopub whose predicate
  isn't classifiable, emit a neutral binding (npa:rolePredicate + raw
  npa:bindingSubject/npa:bindingObject, predicate-discriminated subject) instead of
  dropping it. Known-predicate path unchanged. Gated on the explicit type so we
  don't mint inert rows for incidental IRI-valued triples.
- Materializer: nonAdminTierUpdate's direction-pairing UNION gains two branches that
  join npa:rolePredicate against the tier-pinned RoleDeclaration attached to the
  space; the matched gen:hasInverseProperty/gen:hasRegularProperty fixes both the
  direction and which raw endpoint is the space vs. the agent. The INSERT is
  unchanged, so the materialized row is identical to a pre-classified grant
  (carries npa:inverseProperty/npa:regularProperty) — read side needs no special case.
- New SpacesVocab terms: npa:rolePredicate, npa:bindingSubject, npa:bindingObject.

Tests: extractor emits/gates the neutral binding; end-to-end materializer resolves
both INVERSE and REGULAR custom predicates into the correct ref. Design doc updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tkuhn tkuhn merged commit 7e5bb54 into main Jun 23, 2026
7 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.17.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant