Skip to content

Profiles

Three profiles ship built-in, each extending the previous one:

  • research — the scientific-provenance vocabulary: statement types (claim, finding, result, …) plus evidence and activity.
  • article — adds the shape for representing a paper as a graph: an article root, a summary_point layer (contains / summarizes edges), and quantity / fact detail nodes.
  • project — adds question and hypothesis nodes and the addresses / answers / tests edges that wire a research project's question → hypothesis → test → answer loop.

Validate a graph against a chosen profile with get_profile(name).validate(g) or ideagraph doctor --profile <name>.

A project graph closes its loop with two operations: conclusion_status(g) reports whether the question is answered (a result answers it, backed by evidence, with every addressing hypothesis resolved), and promote(g, article_id=...) carves the project's own new knowledge into a fresh article graph for the cache — gated on conclusion, with edges into imported nodes rewired as cross-article citations. The CLI equivalent is ideagraph promote (--check reports status without promoting).

Built-in knowledge-graph profiles and their semantics.

Importing this package registers the bundled profiles (currently research) so they are available via :func:ideagraph.kg.profile.get_profile. The research-profile semantics (coverage, validation, staleness) are re-exported here for convenience.

ActivityKind

Bases: StrEnum

The type of process an activity represents.

Source code in src/ideagraph/kg/profiles/research.py
66
67
68
69
70
71
72
73
74
class ActivityKind(enum.StrEnum):
    """The type of process an activity represents."""

    COMPUTATION = "computation"
    MEASUREMENT = "measurement"
    ANALYSIS = "analysis"
    REVIEW = "review"
    IMPORT = "import"
    OTHER = "other"

ConclusionResult dataclass

Whether a project graph's question is concluded.

Attributes:

Name Type Description
concluded bool

True when nothing blocks the conclusion.

reasons tuple[str, ...]

Human-readable reasons the project is not concluded (empty when it is).

Source code in src/ideagraph/kg/profiles/project_ops.py
34
35
36
37
38
39
40
41
42
43
44
45
@dataclass(frozen=True)
class ConclusionResult:
    """Whether a project graph's question is concluded.

    Attributes:
        concluded: True when nothing blocks the conclusion.
        reasons: Human-readable reasons the project is not concluded (empty when
            it is).
    """

    concluded: bool
    reasons: tuple[str, ...]

Coverage dataclass

How one assertion node is supported by evidence.

Attributes:

Name Type Description
node_id str

The assertion node's id.

has_own bool

Whether any first-hand evidence supports it.

has_literature bool

Whether any literature evidence supports it.

has_other bool

Whether any other-kind evidence supports it.

evidence_kinds list[str]

The kinds of all supporting evidence, in edge order.

Source code in src/ideagraph/kg/profiles/research_ops.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@dataclass(frozen=True)
class Coverage:
    """How one assertion node is supported by evidence.

    Attributes:
        node_id: The assertion node's id.
        has_own: Whether any first-hand evidence supports it.
        has_literature: Whether any literature evidence supports it.
        has_other: Whether any other-kind evidence supports it.
        evidence_kinds: The kinds of all supporting evidence, in edge order.
    """

    node_id: str
    has_own: bool
    has_literature: bool
    has_other: bool
    evidence_kinds: list[str] = field(default_factory=list)

    @property
    def supported(self) -> bool:
        """Whether the assertion has any supporting evidence."""
        return self.has_own or self.has_literature or self.has_other

    @property
    def category(self) -> str:
        """The support category: unsupported / own / literature / both / other."""
        if self.has_own and self.has_literature:
            return "both"
        if self.has_own:
            return "own"
        if self.has_literature:
            return "literature"
        if self.has_other:
            return "other"
        return "unsupported"

    def to_dict(self) -> dict[str, Any]:
        """Serialise the coverage record.

        Returns:
            A dictionary representation.

        """
        return {
            "node_id": self.node_id,
            "category": self.category,
            "supported": self.supported,
            "evidence_kinds": list(self.evidence_kinds),
        }

category property

The support category: unsupported / own / literature / both / other.

supported property

Whether the assertion has any supporting evidence.

to_dict()

Serialise the coverage record.

Returns:

Type Description
dict[str, Any]

A dictionary representation.

Source code in src/ideagraph/kg/profiles/research_ops.py
82
83
84
85
86
87
88
89
90
91
92
93
94
def to_dict(self) -> dict[str, Any]:
    """Serialise the coverage record.

    Returns:
        A dictionary representation.

    """
    return {
        "node_id": self.node_id,
        "category": self.category,
        "supported": self.supported,
        "evidence_kinds": list(self.evidence_kinds),
    }

EvidenceKind

Bases: StrEnum

The type of artefact a piece of evidence points at.

Source code in src/ideagraph/kg/profiles/research.py
43
44
45
46
47
48
49
50
51
52
53
54
55
class EvidenceKind(enum.StrEnum):
    """The type of artefact a piece of evidence points at."""

    CODE = "code"
    DATA = "data"
    WORKFLOW = "workflow"
    ENVIRONMENT = "environment"
    INSTRUMENT = "instrument"
    FIGURE = "figure"
    TABLE = "table"
    LITERATURE = "literature"
    HUMAN_REVIEW = "human_review"
    OTHER = "other"

EvidenceRelation

Bases: StrEnum

How a piece of evidence bears on a claim.

Source code in src/ideagraph/kg/profiles/research.py
58
59
60
61
62
63
class EvidenceRelation(enum.StrEnum):
    """How a piece of evidence bears on a claim."""

    SUPPORTS = "supports"
    REFUTES = "refutes"
    CONTEXTUAL = "contextual"

NodeType

Bases: StrEnum

The category of a node for relation-endpoint reasoning.

Source code in src/ideagraph/kg/profiles/research.py
77
78
79
80
81
82
83
84
class NodeType(enum.StrEnum):
    """The category of a node for relation-endpoint reasoning."""

    CLAIM = "claim"
    EVIDENCE = "evidence"
    ACTIVITY = "activity"
    ARTEFACT = "artefact"
    AGENT = "agent"

ProvenancePredicate

Bases: StrEnum

The vocabulary of edge types in the research profile.

Source code in src/ideagraph/kg/profiles/research.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class ProvenancePredicate(enum.StrEnum):
    """The vocabulary of edge types in the research profile."""

    SUPPORTED_BY = "supported_by"
    REFUTED_BY = "refuted_by"
    GENERATED_BY = "generated_by"
    USED = "used"
    DERIVED_FROM = "derived_from"
    ATTRIBUTED_TO = "attributed_to"
    REVIEWED_BY = "reviewed_by"
    RELATES_TO = "relates_to"
    ELABORATES = "elaborates"
    CONTRASTS = "contrasts"
    DEPENDS_ON = "depends_on"
    CITES = "cites"
    MOTIVATES = "motivates"
    BUILDS_ON = "builds_on"
    EXTENDS = "extends"
    CONTRADICTS = "contradicts"
    SAME_AS = "same_as"

StatementStatus

Bases: StrEnum

Validation status of an assertion statement.

Source code in src/ideagraph/kg/profiles/research.py
33
34
35
36
37
38
39
40
class StatementStatus(enum.StrEnum):
    """Validation status of an assertion statement."""

    UNRESOLVED = "unresolved"
    VALID = "valid"
    STALE = "stale"
    INVALID = "invalid"
    NEEDS_REVIEW = "needs_review"

StatementType

Bases: StrEnum

The rhetorical role a statement node plays.

Source code in src/ideagraph/kg/profiles/research.py
20
21
22
23
24
25
26
27
28
29
30
class StatementType(enum.StrEnum):
    """The rhetorical role a statement node plays."""

    CLAIM = "claim"
    FINDING = "finding"
    BACKGROUND = "background"
    METHOD = "method"
    DEFINITION = "definition"
    MOTIVATION = "motivation"
    RESULT = "result"
    OTHER = "other"

ValidationResult dataclass

The outcome of validating one assertion node against its evidence.

Attributes:

Name Type Description
node_id str

The validated node's id.

status str

The resolved status token.

supporting list[str]

Ids of evidence linked by supported_by.

refuting list[str]

Ids of evidence linked by refuted_by.

reason str

A human-readable explanation.

Source code in src/ideagraph/kg/profiles/research_ops.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@dataclass(frozen=True)
class ValidationResult:
    """The outcome of validating one assertion node against its evidence.

    Attributes:
        node_id: The validated node's id.
        status: The resolved status token.
        supporting: Ids of evidence linked by ``supported_by``.
        refuting: Ids of evidence linked by ``refuted_by``.
        reason: A human-readable explanation.
    """

    node_id: str
    status: str
    supporting: list[str] = field(default_factory=list)
    refuting: list[str] = field(default_factory=list)
    reason: str = ""

apply_all(graph)

Validate every assertion node and write each status back.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph to validate and update in place.

required

Returns:

Type Description
list[ValidationResult]

The results, one per assertion node.

Source code in src/ideagraph/kg/profiles/research_ops.py
230
231
232
233
234
235
236
237
238
239
240
def apply_all(graph: KnowledgeGraph) -> list[ValidationResult]:
    """Validate every assertion node and write each status back.

    Args:
        graph: The graph to validate and update in place.

    Returns:
        The results, one per assertion node.

    """
    return [apply_validation(graph, nid) for nid in list(graph.nodes) if graph.nodes[nid].type in ASSERTION_TYPES]

apply_validation(graph, node_id)

Validate a node and write the resolved status into its properties.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph.

required
node_id str

The node to validate and update.

required

Returns:

Type Description
ValidationResult

The validation result whose status was applied.

Source code in src/ideagraph/kg/profiles/research_ops.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def apply_validation(graph: KnowledgeGraph, node_id: str) -> ValidationResult:
    """Validate a node and write the resolved status into its properties.

    Args:
        graph: The graph.
        node_id: The node to validate and update.

    Returns:
        The validation result whose status was applied.

    """
    result = validate_node(graph, node_id)
    node = graph.nodes[node_id]
    node.properties["status"] = result.status
    node.touch()
    return result

conclusion_status(graph)

Report whether a project graph's question has been answered.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The project graph.

required

Returns:

Name Type Description
A ConclusionResult

class:ConclusionResult with the blocking reasons, if any.

Source code in src/ideagraph/kg/profiles/project_ops.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def conclusion_status(graph: KnowledgeGraph) -> ConclusionResult:
    """Report whether a project graph's question has been answered.

    Args:
        graph: The project graph.

    Returns:
        A :class:`ConclusionResult` with the blocking reasons, if any.
    """
    reasons: list[str] = []
    questions = graph.nodes_of_type(_QUESTION)
    if not questions:
        reasons.append("no question node in graph")
    for question in questions:
        answers = graph.incoming(question.id, "answers")
        if not answers:
            reasons.append(f"question {question.id!r} has no answering result/finding")
        for answer in answers:
            if not graph.outgoing(answer.source, "supported_by"):
                reasons.append(f"answer {answer.source!r} to {question.id!r} has no supporting evidence")
        for edge in graph.incoming(question.id, "addresses"):
            hypothesis = graph.nodes.get(edge.source)
            status = hypothesis.properties.get("status") if hypothesis else None
            if status not in _RESOLVED:
                reasons.append(f"hypothesis {edge.source!r} is unresolved (status={status!r})")
    return ConclusionResult(concluded=not reasons, reasons=tuple(reasons))

coverage(graph)

Classify support origin for every assertion node in a graph.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph to analyse.

required

Returns:

Type Description
dict[str, Coverage]

A mapping from assertion node id to its coverage.

Source code in src/ideagraph/kg/profiles/research_ops.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def coverage(graph: KnowledgeGraph) -> dict[str, Coverage]:
    """Classify support origin for every assertion node in a graph.

    Args:
        graph: The graph to analyse.

    Returns:
        A mapping from assertion node id to its coverage.

    """
    result: dict[str, Coverage] = {}
    for node in graph.nodes.values():
        if node.type not in ASSERTION_TYPES:
            continue
        own = literature = other = False
        kinds: list[str] = []
        for edge in graph.outgoing(node.id, "supported_by"):
            evidence = graph.nodes.get(edge.target)
            if evidence is None or evidence.type != _EVIDENCE:
                continue
            kind = str(evidence.properties.get("kind", "other"))
            kinds.append(kind)
            if kind == "literature":
                literature = True
            elif kind in _OWN_KINDS:
                own = True
            else:
                other = True
        result[node.id] = Coverage(node.id, own, literature, other, kinds)
    return result

evidence_changed(node, current_digest)

Report whether an evidence node's artefact has drifted.

Parameters:

Name Type Description Default
node Node

The evidence node holding a baseline digest property.

required
current_digest str | None

The artefact's current digest, or None if unknown.

required

Returns:

Type Description
bool

True if both digests are present and differ.

Source code in src/ideagraph/kg/profiles/research_ops.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def evidence_changed(node: Node, current_digest: str | None) -> bool:
    """Report whether an evidence node's artefact has drifted.

    Args:
        node: The evidence node holding a baseline ``digest`` property.
        current_digest: The artefact's current digest, or None if unknown.

    Returns:
        ``True`` if both digests are present and differ.

    """
    baseline = node.properties.get("digest")
    if not baseline or current_digest is None:
        return False
    return baseline != current_digest

find_stale_assertions(graph, resolver)

Return assertion nodes with at least one changed supporting evidence.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph to sweep.

required
resolver DigestResolver

Resolves an evidence node's current digest (or None).

required

Returns:

Type Description
list[Node]

The affected assertion nodes, in insertion order.

Source code in src/ideagraph/kg/profiles/research_ops.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def find_stale_assertions(graph: KnowledgeGraph, resolver: DigestResolver) -> list[Node]:
    """Return assertion nodes with at least one changed supporting evidence.

    Args:
        graph: The graph to sweep.
        resolver: Resolves an evidence node's current digest (or None).

    Returns:
        The affected assertion nodes, in insertion order.

    """
    out: list[Node] = []
    for node in graph.nodes.values():
        if node.type not in ASSERTION_TYPES:
            continue
        for eid in _linked_evidence(graph, node.id, "supported_by"):
            if evidence_changed(graph.nodes[eid], resolver(graph.nodes[eid])):
                out.append(node)
                break
    return out

find_stale_evidence(graph, resolver)

Return evidence nodes whose artefact has changed.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph to sweep.

required
resolver DigestResolver

Resolves an evidence node's current digest (or None).

required

Returns:

Type Description
list[Node]

The changed evidence nodes.

Source code in src/ideagraph/kg/profiles/research_ops.py
263
264
265
266
267
268
269
270
271
272
273
274
def find_stale_evidence(graph: KnowledgeGraph, resolver: DigestResolver) -> list[Node]:
    """Return evidence nodes whose artefact has changed.

    Args:
        graph: The graph to sweep.
        resolver: Resolves an evidence node's current digest (or None).

    Returns:
        The changed evidence nodes.

    """
    return [n for n in graph.nodes.values() if n.type == _EVIDENCE and evidence_changed(n, resolver(n))]

graph_from_legacy(data)

Convert a legacy provenance-graph dict into a KnowledgeGraph.

Parameters:

Name Type Description Default
data dict[str, Any]

A legacy ProvenanceGraph.to_dict() mapping (statements or the pre-v2 claims key, plus evidence/activities/relations/ cross_references).

required

Returns:

Type Description
KnowledgeGraph

The equivalent generic knowledge graph (research profile).

Source code in src/ideagraph/kg/profiles/research_compat.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def graph_from_legacy(data: dict[str, Any]) -> KnowledgeGraph:
    """Convert a legacy provenance-graph dict into a KnowledgeGraph.

    Args:
        data: A legacy ``ProvenanceGraph.to_dict()`` mapping (statements or the
            pre-v2 ``claims`` key, plus evidence/activities/relations/
            cross_references).

    Returns:
        The equivalent generic knowledge graph (research profile).

    """
    graph = KnowledgeGraph(article_id=data.get("article_id"))
    if data.get("metadata") is not None:
        graph.metadata = dict(data["metadata"])
    for item in data.get("statements", data.get("claims", [])):
        graph.add_node(_statement_node(item))
    for item in data.get("evidence", []):
        graph.add_node(_evidence_node(item))
    for item in data.get("activities", []):
        graph.add_node(_activity_node(item))
    for item in data.get("relations", []):
        graph.add_edge(_edge(item, "subject_id", "object_id"))
    for item in data.get("cross_references", []):
        graph.add_edge(_edge(item, "subject_id", "target"))
    return graph

mark_stale(graph, resolver)

Flip affected valid assertion nodes to stale and return them.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph to update in place.

required
resolver DigestResolver

Resolves an evidence node's current digest (or None).

required

Returns:

Type Description
list[Node]

The assertion nodes whose status changed to stale.

Source code in src/ideagraph/kg/profiles/research_ops.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def mark_stale(graph: KnowledgeGraph, resolver: DigestResolver) -> list[Node]:
    """Flip affected ``valid`` assertion nodes to ``stale`` and return them.

    Args:
        graph: The graph to update in place.
        resolver: Resolves an evidence node's current digest (or None).

    Returns:
        The assertion nodes whose status changed to ``stale``.

    """
    changed: list[Node] = []
    for node in find_stale_assertions(graph, resolver):
        if node.properties.get("status") == VALID:
            node.properties["status"] = STALE
            node.touch()
            changed.append(node)
    return changed

promote(graph, *, article_id)

Promote a concluded project's own knowledge into a new article graph.

Keeps every node the project produced itself (those without a source_gid stamp) and the edges among them; edges into imported nodes are rewired as cross-article references to the imported node's origin.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The concluded project graph.

required
article_id str

article_id for the promoted article graph.

required

Returns:

Type Description
KnowledgeGraph

A new :class:KnowledgeGraph holding the promoted knowledge.

Raises:

Type Description
ValueError

If the project is not concluded.

Source code in src/ideagraph/kg/profiles/project_ops.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def promote(graph: KnowledgeGraph, *, article_id: str) -> KnowledgeGraph:
    """Promote a concluded project's own knowledge into a new article graph.

    Keeps every node the project produced itself (those without a
    ``source_gid`` stamp) and the edges among them; edges into imported nodes are
    rewired as cross-article references to the imported node's origin.

    Args:
        graph: The concluded project graph.
        article_id: ``article_id`` for the promoted article graph.

    Returns:
        A new :class:`KnowledgeGraph` holding the promoted knowledge.

    Raises:
        ValueError: If the project is not concluded.
    """
    result = conclusion_status(graph)
    if not result.concluded:
        raise ValueError("project is not concluded: " + "; ".join(result.reasons))

    keep = {nid for nid, node in graph.nodes.items() if SOURCE_GID_KEY not in node.properties}
    out = KnowledgeGraph(article_id=article_id, metadata={"promoted_from": graph.article_id})
    for nid in keep:
        node = graph.nodes[nid]
        out.add_node(
            Node(
                type=node.type,
                text=node.text,
                id=node.id,
                tags=list(node.tags),
                properties=dict(node.properties),
                created_at=node.created_at,
                updated_at=node.updated_at,
            )
        )
    for edge in graph.edges.values():
        if edge.source not in keep:
            continue
        if edge.target in keep:
            _copy_edge(out, edge, edge.target)
            continue
        target = graph.nodes.get(edge.target)
        origin = target.properties.get(SOURCE_GID_KEY) if target else None
        if origin is not None:
            _copy_edge(out, edge, origin)
    return out

validate_all(graph)

Validate every assertion node without mutating the graph.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph to validate.

required

Returns:

Type Description
dict[str, ValidationResult]

A mapping from node id to validation result.

Source code in src/ideagraph/kg/profiles/research_ops.py
199
200
201
202
203
204
205
206
207
208
209
def validate_all(graph: KnowledgeGraph) -> dict[str, ValidationResult]:
    """Validate every assertion node without mutating the graph.

    Args:
        graph: The graph to validate.

    Returns:
        A mapping from node id to validation result.

    """
    return {n.id: validate_node(graph, n.id) for n in graph.nodes.values() if n.type in ASSERTION_TYPES}

validate_node(graph, node_id)

Resolve an assertion node's status from its supporting/refuting evidence.

Parameters:

Name Type Description Default
graph KnowledgeGraph

The graph holding the node and its evidence.

required
node_id str

The id of the node to validate.

required

Returns:

Type Description
ValidationResult

The validation result.

Raises:

Type Description
KeyError

If the node is not held by the graph.

Source code in src/ideagraph/kg/profiles/research_ops.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def validate_node(graph: KnowledgeGraph, node_id: str) -> ValidationResult:
    """Resolve an assertion node's status from its supporting/refuting evidence.

    Args:
        graph: The graph holding the node and its evidence.
        node_id: The id of the node to validate.

    Returns:
        The validation result.

    Raises:
        KeyError: If the node is not held by the graph.

    """
    if node_id not in graph.nodes:
        raise KeyError(node_id)
    supporting = _linked_evidence(graph, node_id, "supported_by")
    refuting = _linked_evidence(graph, node_id, "refuted_by")
    if not supporting and not refuting:
        return ValidationResult(node_id, UNRESOLVED, supporting, refuting, "no supporting or refuting evidence linked")
    if supporting and refuting:
        reason = f"conflicting evidence: {len(supporting)} supporting, {len(refuting)} refuting"
        return ValidationResult(node_id, NEEDS_REVIEW, supporting, refuting, reason)
    if refuting:
        return ValidationResult(node_id, INVALID, supporting, refuting, f"refuted by {len(refuting)} piece(s)")
    return ValidationResult(node_id, VALID, supporting, refuting, f"supported by {len(supporting)} piece(s)")