marimo-pmprov 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: marimo-pmprov
3
+ Version: 0.1.0
4
+ Summary: A marimo anywidget for analytic provenance and annotation, decoupled from pmprov.
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: anywidget>=0.9
7
+ Requires-Dist: marimo>=0.9
8
+ Requires-Dist: traitlets>=5.14
9
+ Provides-Extra: dev
10
+ Requires-Dist: altair; extra == 'dev'
11
+ Requires-Dist: matplotlib; extra == 'dev'
12
+ Requires-Dist: pandas; extra == 'dev'
13
+ Requires-Dist: plotly; extra == 'dev'
14
+ Requires-Dist: pytest>=8.0; extra == 'dev'
15
+ Requires-Dist: vl-convert-python; extra == 'dev'
@@ -0,0 +1,13 @@
1
+ provenance_widget/__init__.py,sha256=C327TiRMza0oQhmTNew5hjdOTfUs7JyJlTd556-ylIM,84
2
+ provenance_widget/display.py,sha256=Q7AMKXrErCRgoLCOiotjet5OaCOykDYVEfefFE2EX18,847
3
+ provenance_widget/interfaces.py,sha256=1IGZM7sdpZUyo_Zxk-BbOx-NCiG0arLR2SLhZWQmHeQ,2518
4
+ provenance_widget/mock_source.py,sha256=K-THrXAtBZM4aJXMnyQqYUZ99Q5_yMT7fH6LMUGO_mY,11429
5
+ provenance_widget/pmprov_adapter.py,sha256=2P40gCQKfhMVLOzGEf7FN6TXsKMr84hUxwq29FZKQyA,7404
6
+ provenance_widget/serialize.py,sha256=BY36NuU9uJEESqSRmEul9axtqBKmFK8x_m_I_7BMMOI,2237
7
+ provenance_widget/widget.py,sha256=IUsstc6zHxK71KFK7F5veF8c2lm-JMh-BLyJxmAvLT0,3196
8
+ provenance_widget/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ provenance_widget/static/widget.css,sha256=uPeA5sasZqvO_T7CQK4Q2aCeFxt51BnB36kz6HCnpdM,6023
10
+ provenance_widget/static/widget.js,sha256=ni9TMSwo7ldg7vbKVdGNSAL7LG8VGCUwLJ_0hABttp4,20777
11
+ marimo_pmprov-0.1.0.dist-info/METADATA,sha256=XLzJhZWCd9U5NBjhYXr7ClaxHO19plWgy3lbK2nce0c,528
12
+ marimo_pmprov-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
13
+ marimo_pmprov-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1 @@
1
+ """marimo-pmprov: provenance and annotation sidebar widget for marimo notebooks."""
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import html
4
+
5
+ import marimo as mo
6
+
7
+
8
+ def _wrap(inner_html: str, artifact_id: str, artifact_name: str, granularity: str) -> mo.Html:
9
+ return mo.Html(
10
+ f'<div class="pw-annotatable" '
11
+ f'data-pw-artifact-id="{html.escape(artifact_id, quote=True)}" '
12
+ f'data-pw-artifact-name="{html.escape(artifact_name, quote=True)}" '
13
+ f'data-pw-granularity="{html.escape(granularity, quote=True)}">'
14
+ f"{inner_html}"
15
+ f"</div>"
16
+ )
17
+
18
+
19
+ def show_table(table: "mo.ui.table", artifact_id: str, artifact_name: str) -> mo.Html:
20
+ return _wrap(table.text, artifact_id, artifact_name, granularity="dataset")
21
+
22
+
23
+ def show_chart(chart_html: mo.Html, artifact_id: str, artifact_name: str) -> mo.Html:
24
+ return _wrap(chart_html.text, artifact_id, artifact_name, granularity="chart_selection")
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Protocol
6
+
7
+
8
+ class StepCategory(str, Enum):
9
+ DATA_LOADING = "DATA_LOADING"
10
+ CLEANING = "CLEANING"
11
+ FEATURE_ENGINEERING = "FEATURE_ENGINEERING"
12
+ AGGREGATION = "AGGREGATION"
13
+ COMPARISON = "COMPARISON"
14
+ ANALYSIS = "ANALYSIS"
15
+
16
+
17
+ @dataclass
18
+ class Agent:
19
+ agent_id: str
20
+ agent_type: str
21
+ display_name: str
22
+
23
+
24
+ @dataclass
25
+ class ColumnAdded:
26
+ name: str
27
+ dtype: str
28
+
29
+
30
+ @dataclass
31
+ class ColumnRemoved:
32
+ name: str
33
+ dtype: str
34
+
35
+
36
+ @dataclass
37
+ class Delta:
38
+ columns_added: list[ColumnAdded]
39
+ columns_removed: list[ColumnRemoved]
40
+ row_count_before: int
41
+ row_count_after: int
42
+
43
+ def summary(self) -> str:
44
+ parts = []
45
+ for c in self.columns_added:
46
+ parts.append(f"+{c.name}")
47
+ for c in self.columns_removed:
48
+ parts.append(f"-{c.name}")
49
+ parts.append(f"rows {self.row_count_before}->{self.row_count_after}")
50
+ return ", ".join(parts)
51
+
52
+
53
+ @dataclass
54
+ class Operation:
55
+ operation_id: str
56
+ name: str
57
+ operation_type: str
58
+ command_name: str
59
+ category: StepCategory
60
+
61
+
62
+ @dataclass
63
+ class ArtifactRef:
64
+ artifact_id: str
65
+ artifact_name: str
66
+ granularity: str
67
+ detail: str | None = None
68
+
69
+
70
+ @dataclass
71
+ class Tag:
72
+ name: str
73
+ color: str
74
+
75
+
76
+ @dataclass
77
+ class Annotation:
78
+ annotation_id: str
79
+ title: str
80
+ note: str
81
+ tags: list[Tag]
82
+ artifacts: list[ArtifactRef]
83
+ author: Agent
84
+ timestamp: str
85
+
86
+
87
+ @dataclass
88
+ class ProvenanceNode:
89
+ state_id: str
90
+ parent_state_id: str | None
91
+ branch_id: str
92
+ operation: Operation
93
+ params: dict
94
+ delta: Delta
95
+ annotations: list[Annotation] = field(default_factory=list)
96
+
97
+
98
+ @dataclass
99
+ class ProvenanceTree:
100
+ nodes: dict[str, ProvenanceNode]
101
+ root_id: str
102
+ branches: dict[str, str]
103
+
104
+ def children_of(self, state_id: str) -> list[ProvenanceNode]:
105
+ return [n for n in self.nodes.values() if n.parent_state_id == state_id]
106
+
107
+
108
+ class ProvenanceSource(Protocol):
109
+ def get_tree(self) -> ProvenanceTree: ...
110
+
111
+ def state_for_artifact(self, artifact_id: str) -> str | None:
112
+ """Return the state_id of the analysis state that contains/produced
113
+ this artifact, or None if the source can't resolve it.
114
+
115
+ Used to associate a new annotation with the right tree node purely
116
+ from the artifact it targets, without the caller having to thread a
117
+ state_id through by hand.
118
+ """
119
+ ...
@@ -0,0 +1,207 @@
1
+ from __future__ import annotations
2
+
3
+ from provenance_widget.interfaces import (
4
+ Agent, Annotation, ArtifactRef, ColumnAdded, ColumnRemoved, Delta,
5
+ Operation, ProvenanceNode, ProvenanceTree, StepCategory, Tag,
6
+ )
7
+
8
+ _STUDENT = Agent(agent_id="agent-student", agent_type="human", display_name="S")
9
+
10
+
11
+ def _op(op_id: str, name: str, op_type: str, command: str, category: StepCategory) -> Operation:
12
+ return Operation(operation_id=op_id, name=name, operation_type=op_type,
13
+ command_name=command, category=category)
14
+
15
+
16
+ class MockProvenanceSource:
17
+ """Deterministic in-memory ProvenanceSource for demoing the widget without pmprov."""
18
+
19
+ def get_tree(self) -> ProvenanceTree:
20
+ root = ProvenanceNode(
21
+ state_id="s0", parent_state_id=None, branch_id="main",
22
+ operation=_op("op0", "Load event log", "IMPORT", "load_event_log",
23
+ StepCategory.DATA_LOADING),
24
+ params={"filepath": "data/rtfm_full.csv"},
25
+ delta=Delta([ColumnAdded("case:concept:name", "object"),
26
+ ColumnAdded("concept:name", "object"),
27
+ ColumnAdded("time:timestamp", "datetime64[ns]")],
28
+ [], row_count_before=0, row_count_after=15214),
29
+ annotations=[],
30
+ )
31
+
32
+ filtered = ProvenanceNode(
33
+ state_id="s1", parent_state_id="s0", branch_id="main",
34
+ operation=_op("op1", "Filter long traces", "FILTER", "filter_duration",
35
+ StepCategory.CLEANING),
36
+ params={"threshold_hours": 120},
37
+ delta=Delta([], [], row_count_before=15214, row_count_after=14032),
38
+ annotations=[Annotation(
39
+ annotation_id="ann-c8f2", title="Truncate outliers",
40
+ note="Removed traces longer than 120h; distorted the mean duration.",
41
+ tags=[Tag(name="cleaning", color="#0EA5E9"), Tag(name="v1.1", color="#94A3B8")],
42
+ artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
43
+ granularity="column", detail="duration")],
44
+ author=_STUDENT, timestamp="2026-08-04T10:42:00",
45
+ )],
46
+ )
47
+
48
+ enriched = ProvenanceNode(
49
+ state_id="s2", parent_state_id="s1", branch_id="main",
50
+ operation=_op("op2", "Calculate case cost", "ENRICHMENT", "add_attribute",
51
+ StepCategory.FEATURE_ENGINEERING),
52
+ params={"output_col": "case_cost", "source": "resource_wage_matrix"},
53
+ delta=Delta([ColumnAdded("case_cost", "float64")], [],
54
+ row_count_before=14032, row_count_after=14032),
55
+ annotations=[Annotation(
56
+ annotation_id="ann-a911", title="Calculate case cost",
57
+ note="Joined hourly wage matrix with resource trace logs.",
58
+ tags=[Tag(name="cost", color="#10B981"), Tag(name="enrichment", color="#10B981")],
59
+ artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
60
+ granularity="dataset")],
61
+ author=_STUDENT, timestamp="2026-08-04T10:58:00",
62
+ )],
63
+ )
64
+
65
+ explore_threshold = ProvenanceNode(
66
+ state_id="s1b", parent_state_id="s0", branch_id="branch-explore",
67
+ operation=_op("op1b", "Filter long traces (60h)", "FILTER", "filter_duration",
68
+ StepCategory.CLEANING),
69
+ params={"threshold_hours": 60},
70
+ delta=Delta([], [], row_count_before=15214, row_count_after=11800),
71
+ annotations=[Annotation(
72
+ annotation_id="ann-e771", title="Tighter threshold experiment",
73
+ note="Tried a stricter 60h cutoff; dropped too many valid cases, abandoned.",
74
+ tags=[Tag(name="cleaning", color="#0EA5E9"), Tag(name="exploration", color="#A855F7")],
75
+ artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
76
+ granularity="row_subset", detail="rows 1-11800")],
77
+ author=_STUDENT, timestamp="2026-08-04T10:47:00",
78
+ )],
79
+ )
80
+
81
+ rule_induction = ProvenanceNode(
82
+ state_id="s3a", parent_state_id="s2", branch_id="branch-a",
83
+ operation=_op("op3a", "Induce decision rules", "RULE_INDUCTION", "induce_rules",
84
+ StepCategory.ANALYSIS),
85
+ params={"target_col": "is_long_case", "max_depth": 4},
86
+ delta=Delta([ColumnAdded("predicted_outcome", "bool")], [],
87
+ row_count_before=14032, row_count_after=14032),
88
+ annotations=[Annotation(
89
+ annotation_id="ann-d213", title="First rule set",
90
+ note="Depth-4 tree overfits on resource attribute; needs pruning.",
91
+ tags=[Tag(name="rules", color="#F59E0B")],
92
+ artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
93
+ granularity="row_subset", detail="rows 1-4032")],
94
+ author=_STUDENT, timestamp="2026-08-04T11:10:00",
95
+ )],
96
+ )
97
+
98
+ conformance = ProvenanceNode(
99
+ state_id="s3b", parent_state_id="s2", branch_id="branch-b",
100
+ operation=_op("op3b", "Conformance check", "CONFORMANCE_CHECK", "check_conformance",
101
+ StepCategory.COMPARISON),
102
+ params={"model": "petri_net_v1"},
103
+ delta=Delta([ColumnAdded("fitness", "float64")], [],
104
+ row_count_before=14032, row_count_after=14032),
105
+ annotations=[Annotation(
106
+ annotation_id="ann-f004", title="Baseline fitness",
107
+ note="Fitness 0.87 against the reference Petri net.",
108
+ tags=[Tag(name="conformance", color="#F43F5E"), Tag(name="v1.1", color="#94A3B8")],
109
+ artifacts=[ArtifactRef(artifact_id="art-process-map", artifact_name="Chart_ProcessMap",
110
+ granularity="chart_selection")],
111
+ author=_STUDENT, timestamp="2026-08-04T11:22:00",
112
+ )],
113
+ )
114
+
115
+ pruned_rules = ProvenanceNode(
116
+ state_id="s4a", parent_state_id="s3a", branch_id="branch-a",
117
+ operation=_op("op4a", "Prune rule set", "RULE_PRUNING", "prune_rules",
118
+ StepCategory.ANALYSIS),
119
+ params={"max_depth": 2, "min_support": 0.05},
120
+ delta=Delta([], [], row_count_before=14032, row_count_after=14032),
121
+ annotations=[Annotation(
122
+ annotation_id="ann-b552", title="Pruned rule set",
123
+ note="Capped depth at 2; resource attribute no longer dominates the split.",
124
+ tags=[Tag(name="rules", color="#F59E0B"), Tag(name="v1.2", color="#94A3B8")],
125
+ artifacts=[ArtifactRef(artifact_id="art-rule-report", artifact_name="rule_report.json",
126
+ granularity="dataset")],
127
+ author=_STUDENT, timestamp="2026-08-04T11:35:00",
128
+ )],
129
+ )
130
+
131
+ cost_aggregation = ProvenanceNode(
132
+ state_id="s4b", parent_state_id="s3b", branch_id="branch-b",
133
+ operation=_op("op4b", "Aggregate cost by outcome", "GROUP_AGGREGATE", "groupby_agg",
134
+ StepCategory.AGGREGATION),
135
+ params={"group_by": "fitness_bucket", "agg": "mean(case_cost)"},
136
+ delta=Delta([ColumnAdded("mean_case_cost", "float64")], [],
137
+ row_count_before=14032, row_count_after=6),
138
+ annotations=[Annotation(
139
+ annotation_id="ann-9a10", title="Cost by conformance bucket",
140
+ note="Non-conformant cases cost 2.3x more on average — worth flagging to reviewer.",
141
+ tags=[Tag(name="cost", color="#10B981"), Tag(name="conformance", color="#F43F5E")],
142
+ artifacts=[ArtifactRef(artifact_id="art-event-log", artifact_name="event_log.csv",
143
+ granularity="column", detail="case_cost")],
144
+ author=_STUDENT, timestamp="2026-08-04T11:48:00",
145
+ )],
146
+ )
147
+
148
+ reviewer_check = ProvenanceNode(
149
+ state_id="s5a", parent_state_id="s4a", branch_id="branch-a",
150
+ operation=_op("op5a", "Reviewer sign-off", "REVIEW", "review_annotation",
151
+ StepCategory.COMPARISON),
152
+ params={"reviewer": "agent-reviewer-1"},
153
+ delta=Delta([], [], row_count_before=14032, row_count_after=14032),
154
+ annotations=[Annotation(
155
+ annotation_id="ann-7c31", title="Reviewer approved pruning",
156
+ note="Confirmed the pruned rule set generalizes on a held-out fold.",
157
+ tags=[Tag(name="rules", color="#F59E0B"), Tag(name="review", color="#3B82F6")],
158
+ artifacts=[ArtifactRef(artifact_id="art-rule-report", artifact_name="rule_report.json",
159
+ granularity="dataset")],
160
+ author=Agent(agent_id="agent-reviewer-1", agent_type="human", display_name="R"),
161
+ timestamp="2026-08-04T13:05:00",
162
+ )],
163
+ )
164
+
165
+ cost_model = ProvenanceNode(
166
+ state_id="s5b", parent_state_id="s4b", branch_id="branch-b",
167
+ operation=_op("op5b", "Fit cost prediction model", "MODEL_FIT", "fit_regressor",
168
+ StepCategory.FEATURE_ENGINEERING),
169
+ params={"model": "gradient_boost", "target": "mean_case_cost"},
170
+ delta=Delta([ColumnAdded("predicted_cost", "float64")], [],
171
+ row_count_before=6, row_count_after=6),
172
+ annotations=[Annotation(
173
+ annotation_id="ann-1de4", title="Cost prediction model",
174
+ note="R^2 0.81 on the 6 conformance buckets; small sample, treat as directional.",
175
+ tags=[Tag(name="cost", color="#10B981"), Tag(name="v1.2", color="#94A3B8")],
176
+ artifacts=[ArtifactRef(artifact_id="art-cost-model", artifact_name="cost_model.pkl",
177
+ granularity="dataset")],
178
+ author=_STUDENT, timestamp="2026-08-04T12:15:00",
179
+ )],
180
+ )
181
+
182
+ return ProvenanceTree(
183
+ nodes={n.state_id: n for n in [
184
+ root, filtered, explore_threshold, enriched,
185
+ rule_induction, conformance, pruned_rules, cost_aggregation,
186
+ reviewer_check, cost_model,
187
+ ]},
188
+ root_id="s0",
189
+ branches={
190
+ "main": "main",
191
+ "branch-explore": "explore-threshold",
192
+ "branch-a": "rule-induction",
193
+ "branch-b": "conformance",
194
+ },
195
+ )
196
+
197
+ def state_for_artifact(self, artifact_id: str) -> str | None:
198
+ """Best-effort: MockProvenanceSource has no artifact registry either
199
+ (its ArtifactRefs are just hand-authored strings) — fall back to
200
+ whichever node's own baked-in annotations already reference this
201
+ artifact_id, since that's the closest thing to "produced it" here.
202
+ """
203
+ for node in self.get_tree().nodes.values():
204
+ for annotation in node.annotations:
205
+ if any(a.artifact_id == artifact_id for a in annotation.artifacts):
206
+ return node.state_id
207
+ return None
@@ -0,0 +1,175 @@
1
+ """ProvenanceSource adapter backed by a live pmprov RuntimeTracker.
2
+
3
+ pmprov (https://github.com/alvarobernal2412/pmprov) records provenance
4
+ automatically at the AST level — no explicit annotation concept exists there
5
+ (see the pmprov README's own note on this). This adapter translates its
6
+ state/step graph into the ProvenanceTree shape this widget renders; all
7
+ annotation still happens on the marimo-pmprov side via
8
+ `ProvenancePanel.commit_annotation()` or the sidebar's composer.
9
+
10
+ Only imports pmprov lazily inside methods — marimo-pmprov itself has no
11
+ hard dependency on pmprov (see the `pmprov` extra in pyproject.toml).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ from provenance_widget.interfaces import (
19
+ ColumnAdded,
20
+ ColumnRemoved,
21
+ Delta,
22
+ Operation,
23
+ ProvenanceNode,
24
+ ProvenanceTree,
25
+ StepCategory,
26
+ )
27
+
28
+ _DEFAULT_CATEGORY = StepCategory.ANALYSIS
29
+
30
+
31
+ def _map_category(name: str | None) -> StepCategory:
32
+ """Map a pmprov step_category name onto this widget's fixed enum.
33
+
34
+ Register pmprov step categories with the exact StepCategory member names
35
+ (DATA_LOADING, CLEANING, FEATURE_ENGINEERING, AGGREGATION, COMPARISON,
36
+ ANALYSIS) via `tracker.step_category(...)` for a 1:1 mapping — anything
37
+ else (including pmprov's "unknown" default) falls back to ANALYSIS.
38
+ """
39
+ if not name:
40
+ return _DEFAULT_CATEGORY
41
+ try:
42
+ return StepCategory[name.upper()]
43
+ except KeyError:
44
+ return _DEFAULT_CATEGORY
45
+
46
+
47
+ def _root_node(state_id: str, branch_id: str) -> ProvenanceNode:
48
+ return ProvenanceNode(
49
+ state_id=state_id,
50
+ parent_state_id=None,
51
+ branch_id=branch_id,
52
+ operation=Operation(
53
+ operation_id="root",
54
+ name="Session start",
55
+ operation_type="IMPORT",
56
+ command_name="init",
57
+ category=StepCategory.DATA_LOADING,
58
+ ),
59
+ params={},
60
+ delta=Delta([], [], row_count_before=0, row_count_after=0),
61
+ annotations=[],
62
+ )
63
+
64
+
65
+ class PmprovAdapter:
66
+ """Reads a pmprov RuntimeTracker's history as a ProvenanceTree.
67
+
68
+ Unlike MockProvenanceSource's fixed snapshot, this tree grows as the
69
+ notebook runs — call `ProvenancePanel.refresh()` after each traced step
70
+ (once its writes have settled; see `settle()` in the pmprov README's
71
+ Marimo snippet) to pick up the new state.
72
+ """
73
+
74
+ def __init__(self, tracker: Any) -> None:
75
+ self._rt = tracker
76
+
77
+ def get_tree(self) -> ProvenanceTree:
78
+ history_id = self._rt._history.history_id
79
+ graph = self._rt.storage.load_graph(history_id)
80
+ states_by_id = {s["state_id"]: s for s in graph["states"]}
81
+ steps = sorted(graph["steps"], key=lambda s: s["timestamp"])
82
+
83
+ # pmprov's DuckDB/SQLite backend stores this as "" (empty string),
84
+ # not NULL, for the root state — falsy check, not an `is None` one.
85
+ root_state = next(
86
+ (s for s in states_by_id.values() if not s["produced_by_step_id"]), None
87
+ )
88
+ if root_state is None:
89
+ # RuntimeTracker always writes its root state synchronously at
90
+ # init, so this only happens if get_tree() runs before that —
91
+ # fall back to an empty-but-valid single-node tree.
92
+ return ProvenanceTree(
93
+ nodes={"root": _root_node("root", "main")}, root_id="root", branches={"main": "main"}
94
+ )
95
+
96
+ root_id = root_state["state_id"]
97
+ nodes: dict[str, ProvenanceNode] = {root_id: _root_node(root_id, root_state["branch_id"])}
98
+ branches: dict[str, str] = {root_state["branch_id"]: "main"}
99
+
100
+ # deltas only carry a net rows_delta, not before/after counts, so
101
+ # row counts are approximated by walking the pipeline in order and
102
+ # accumulating from 0 at the root — accurate for the common linear
103
+ # case this adapter is meant for, not for arbitrary branch topologies.
104
+ running_rows: dict[str, int] = {root_id: 0}
105
+
106
+ for step in steps:
107
+ output_id = step["output_state_id"]
108
+ state = states_by_id.get(output_id)
109
+ if state is None:
110
+ continue
111
+
112
+ detail = self._rt.storage.load_state_detail(output_id)
113
+ op_info = detail.get("operation") or {}
114
+ delta_info = detail.get("delta") or {}
115
+ branch_name = detail.get("branch_name") or state["branch_id"]
116
+ branches.setdefault(state["branch_id"], branch_name)
117
+
118
+ parent_id = step["input_state_id"]
119
+ rows_before = running_rows.get(parent_id, 0)
120
+ rows_after = rows_before + (delta_info.get("rows_delta") or 0)
121
+ running_rows[output_id] = rows_after
122
+
123
+ nodes[output_id] = ProvenanceNode(
124
+ state_id=output_id,
125
+ parent_state_id=parent_id,
126
+ branch_id=state["branch_id"],
127
+ operation=Operation(
128
+ operation_id=step["step_id"],
129
+ name=op_info.get("name") or step["func_name"],
130
+ operation_type=op_info.get("type") or "unknown",
131
+ command_name=step["func_name"],
132
+ category=_map_category(op_info.get("category")),
133
+ ),
134
+ params={p["param_id"]: p["value"] for p in detail.get("params", [])},
135
+ delta=Delta(
136
+ columns_added=[ColumnAdded(name=c, dtype="") for c in delta_info.get("columns_added", [])],
137
+ columns_removed=[
138
+ ColumnRemoved(name=c, dtype="") for c in delta_info.get("columns_removed", [])
139
+ ],
140
+ row_count_before=rows_before,
141
+ row_count_after=rows_after,
142
+ ),
143
+ annotations=[],
144
+ )
145
+
146
+ return ProvenanceTree(nodes=nodes, root_id=root_id, branches=branches)
147
+
148
+ def state_for_artifact(self, artifact_id: str) -> str | None:
149
+ """pmprov has no separate artifact registry — a "state" *is* the
150
+ DataFrame/artifact a step produced, so an artifact_id built from
151
+ `latest_state_id()` (the convention this adapter's callers use, see
152
+ the demo notebook) is already a real state_id. Confirm it still
153
+ exists in the current graph before handing it back.
154
+ """
155
+ history_id = self._rt._history.history_id
156
+ graph = self._rt.storage.load_graph(history_id)
157
+ if any(s["state_id"] == artifact_id for s in graph["states"]):
158
+ return artifact_id
159
+ return None
160
+
161
+ def latest_state_id(self, func_name: str | None = None) -> str | None:
162
+ """Return the output_state_id of the most recent step.
163
+
164
+ Pass func_name to disambiguate ("the last time X ran") if steps have
165
+ happened since; omit it to just mean "whatever ran last". Convenience
166
+ for notebooks that want to annotate "the step I just ran" without
167
+ threading state_ids through by hand — settle() first (see pmprov's
168
+ README) so the write has landed before this reads it.
169
+ """
170
+ history_id = self._rt._history.history_id
171
+ steps = self._rt.storage.load_graph(history_id)["steps"]
172
+ candidates = steps if func_name is None else [s for s in steps if s["func_name"] == func_name]
173
+ if not candidates:
174
+ return None
175
+ return max(candidates, key=lambda s: s["timestamp"])["output_state_id"]
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+
5
+ from provenance_widget.interfaces import ProvenanceTree
6
+
7
+
8
+ def tree_to_json(tree: ProvenanceTree) -> dict:
9
+ nodes = {}
10
+ for state_id, node in tree.nodes.items():
11
+ node_dict = asdict(node)
12
+ node_dict["operation"]["category"] = node.operation.category.value
13
+ nodes[state_id] = {
14
+ "stateId": node_dict["state_id"],
15
+ "parentStateId": node_dict["parent_state_id"],
16
+ "branchId": node_dict["branch_id"],
17
+ "operation": {
18
+ "operationId": node_dict["operation"]["operation_id"],
19
+ "name": node_dict["operation"]["name"],
20
+ "operationType": node_dict["operation"]["operation_type"],
21
+ "commandName": node_dict["operation"]["command_name"],
22
+ "category": node_dict["operation"]["category"],
23
+ },
24
+ "params": node_dict["params"],
25
+ "delta": {
26
+ "columnsAdded": node_dict["delta"]["columns_added"],
27
+ "columnsRemoved": node_dict["delta"]["columns_removed"],
28
+ "rowCountBefore": node_dict["delta"]["row_count_before"],
29
+ "rowCountAfter": node_dict["delta"]["row_count_after"],
30
+ "summary": node.delta.summary(),
31
+ },
32
+ "annotations": [
33
+ {
34
+ "annotationId": a["annotation_id"],
35
+ "title": a["title"],
36
+ "note": a["note"],
37
+ "tags": a["tags"],
38
+ "artifacts": [
39
+ {
40
+ "artifactId": ar["artifact_id"],
41
+ "artifactName": ar["artifact_name"],
42
+ "granularity": ar["granularity"],
43
+ "detail": ar["detail"],
44
+ }
45
+ for ar in a["artifacts"]
46
+ ],
47
+ "author": a["author"],
48
+ "timestamp": a["timestamp"],
49
+ }
50
+ for a in node_dict["annotations"]
51
+ ],
52
+ }
53
+ return {
54
+ "rootId": tree.root_id,
55
+ "branches": tree.branches,
56
+ "nodes": nodes,
57
+ }
File without changes
@@ -0,0 +1 @@
1
+ .pw-root{font-family:ui-sans-serif,system-ui,sans-serif;color:#0f172a;background:#f8fafc;padding:8px;border-radius:8px;min-width:0;overflow:hidden}@media (prefers-color-scheme: dark){.pw-root{color:#f8fafc;background:#0f172a}}.pw-root.pw-collapsed .pw-content{display:none}body.pw-picking-active *{cursor:crosshair!important}.pw-segmented-control{display:flex;gap:4px;margin-bottom:8px;min-width:0}.pw-segment{border:1px solid #94A3B8;background:transparent;border-radius:6px;padding:4px 10px;cursor:pointer}.pw-segment-active{background:#64748b;color:#fff}.pw-node-card,.pw-commit-card{border:1px solid #E2E8F0;border-left:4px solid var(--category-color, #64748B);border-radius:10px;padding:8px 10px;margin-bottom:8px;cursor:pointer;background:#fff}@media (prefers-color-scheme: dark){.pw-node-card,.pw-commit-card{background:#1e293b;border-color:#334155}}.pw-node-card.pw-picker-hover,.pw-annotatable.pw-picker-hover{outline:2px solid #F59E0B}.pw-node-card-selected{outline:2px solid #0EA5E9}.pw-node-card-collapsed{padding:6px 10px;margin-bottom:4px}.pw-node-card-collapsed .pw-node-title{font-weight:500;opacity:.55;margin:0}.pw-op-badge{font-size:11px;text-transform:uppercase;color:var(--category-color, #64748B);font-weight:600}.pw-node-title{font-weight:700;margin:2px 0}.pw-node-note{font-size:12px;color:#64748b;margin-bottom:4px}.pw-node-tags,.pw-node-artifacts{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px;min-width:0}.pw-node-no-annotations{font-size:12px;color:#94a3b8;font-style:italic;margin-top:4px}.pw-annotation-block{border-top:1px dashed #E2E8F0;margin-top:6px;padding-top:6px}@media (prefers-color-scheme: dark){.pw-annotation-block{border-color:#334155}}.pw-annotation-title{font-size:12px;font-weight:600;color:#334155;margin-bottom:2px}@media (prefers-color-scheme: dark){.pw-annotation-title{color:#cbd5e1}}.pw-tag-chip{background:color-mix(in srgb,var(--chip-color, #64748B) 20%,transparent);color:var(--chip-color, #64748B);border-radius:999px;padding:2px 8px;font-size:11px}.pw-restore-btn{margin-left:6px;border:none;background:#f59e0b;color:#fff;border-radius:999px;padding:1px 6px;font-size:10px;cursor:pointer}.pw-artifact-badge{border:1px solid #94A3B8;border-radius:6px;padding:2px 6px;font-size:11px;font-family:ui-monospace,monospace}.pw-columns-scroll{flex:1 1 auto;min-height:0;overflow:auto}.pw-columns{display:flex;gap:12px;min-width:0;position:relative}.pw-column{min-width:260px;position:relative;z-index:1}.pw-tree-lines{position:absolute;top:0;left:0;z-index:0;pointer-events:none;overflow:visible}.pw-tree-line{fill:none;stroke:#cbd5e1;stroke-width:1.5}.pw-tree-line-active{fill:none;stroke:#0ea5e9;stroke-width:2}@media (prefers-color-scheme: dark){.pw-tree-line{stroke:#334155}}.pw-breadcrumb{flex:0 0 auto;font-size:12px;color:#64748b;margin-bottom:8px}.pw-breadcrumb-sep{margin:0 6px}.pw-filter-bar{flex:0 0 auto;margin-bottom:8px}.pw-filter-row{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:6px;min-width:0}.pw-filter-label{font-size:11px;text-transform:uppercase;color:#64748b;font-weight:600;min-width:90px}.pw-filter-chip{border:1px solid #94A3B8;border-radius:999px;background:transparent;cursor:pointer;padding:3px 10px;font-size:12px}.pw-filter-chip-active{background:#0ea5e9;border-color:#0ea5e9;color:#fff}.pw-filter-chip-empty{border-style:dashed;color:#64748b;font-style:italic;cursor:default}.pw-tree-inspect-pane{flex:0 0 auto;margin-top:12px;border:1px solid #CBD5E1;border-radius:8px;background:#f1f5f9;padding:10px 14px 14px;max-height:38%;overflow:auto}@media (prefers-color-scheme: dark){.pw-tree-inspect-pane{border-color:#334155;background:#1e293b}}.pw-tree-inspect-pane-label{font-size:11px;text-transform:uppercase;letter-spacing:.04em;font-weight:700;color:#475569;margin-bottom:8px}@media (prefers-color-scheme: dark){.pw-tree-inspect-pane-label{color:#94a3b8}}.pw-tree-inspect-pane-empty{color:#64748b;font-size:13px;font-style:italic}.pw-tree-portal .pw-column{min-width:300px}.pw-tree-header{flex:0 0 auto;margin-bottom:8px}.pw-tree-title{font-weight:700;font-size:15px}.pw-tree-caption{font-size:12px;color:#64748b;margin-top:2px}.pw-inspect-empty{padding:24px 8px;text-align:center;color:#64748b;font-size:13px}.pw-diff-pane{font-family:ui-monospace,monospace;font-size:12px;margin:8px 0}.pw-diff-added{color:#10b981}.pw-diff-removed{color:#f43f5e}.pw-composer{display:flex;flex-direction:column;gap:6px;border-top:1px solid #E2E8F0;padding-top:8px;margin-top:8px}.pw-composer textarea{min-height:60px}.pw-target-selector{border:1px solid #94A3B8;border-radius:6px;padding:6px 8px;font-size:12px}.pw-target-selector-empty{color:#64748b;font-style:italic}.pw-target-selector-label{font-size:10px;text-transform:uppercase;color:#64748b;font-weight:600;margin-bottom:2px}.pw-target-selector-value{font-family:ui-monospace,monospace;color:#0f172a}@media (prefers-color-scheme: dark){.pw-target-selector-value{color:#f8fafc}}.pw-commit-btn{background:#0f172a;color:#fff;border:none;border-radius:6px;padding:8px;cursor:pointer}@media (prefers-color-scheme: dark){.pw-commit-btn{background:#f8fafc;color:#0f172a}}.pw-config-heading{font-size:11px;text-transform:uppercase;color:#64748b;font-weight:600;margin-bottom:6px}.pw-config-radio{display:flex;align-items:center;gap:6px;padding:4px 0;cursor:pointer;text-transform:capitalize}.pw-restore-banner{background:#fef3c7;border:1px solid #F59E0B;border-radius:6px;padding:8px;margin-bottom:8px;font-size:13px}.pw-pick-btn{border:1px dashed #94A3B8;background:transparent;border-radius:6px;padding:6px;cursor:pointer;margin-top:4px}.pw-pick-btn-armed{border:1px solid #F59E0B;background:color-mix(in srgb,#F59E0B 15%,transparent);color:#f59e0b;font-weight:600}.pw-gutter-plus{width:20px;height:20px;border-radius:50%;border:none;background:#0ea5e9;color:#fff;font-size:14px;line-height:1;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;box-shadow:0 1px 4px #0f172a59;z-index:1000}.pw-gutter-plus:hover{background:#0284c7}
@@ -0,0 +1,2 @@
1
+ function U(n,t,e){let o=document.createElement("div");return o.className="pw-segmented-control",n.forEach(({value:r,label:i})=>{let a=document.createElement("button");a.textContent=i,a.className=r===t?"pw-segment pw-segment-active":"pw-segment",a.addEventListener("click",()=>e(r)),o.appendChild(a)}),o}function B(n){let t=document.createElement("span");return t.className="pw-tag-chip",t.textContent=`#${n.name}`,t.style.setProperty("--chip-color",n.color),t}var z={dataset:"Whole dataset",column:"Column",row_subset:"Row subset",cell:"Cell",chart_selection:"Chart selection"};function _(n){let t=document.createElement("span");t.className="pw-artifact-badge";let e=z[n.granularity]??n.granularity;return t.textContent=n.detail?`${n.artifactName} \xB7 ${e}: ${n.detail}`:`${n.artifactName} \xB7 ${e}`,t.dataset.artifactId=n.artifactId,t}function me(n){let t=[];for(let e of Object.values(n.nodes))for(let o of e.annotations)t.push({stateId:e.stateId,annotation:o});for(let e of Y)t.push(e);return t.sort((e,o)=>e.annotation.timestamp<o.annotation.timestamp?-1:1)}var Y=[];function ue(n,t,e){let o=document.createElement("div");o.className="pw-commit-card",o.dataset.stateId=n.stateId;let r=document.createElement("div");r.className="pw-node-title",r.textContent=n.annotation.title,o.appendChild(r);let i=document.createElement("div");i.className="pw-node-tags",n.annotation.tags.forEach(s=>{let c=B(s);if(t==="reviewer"){let l=document.createElement("button");l.className="pw-restore-btn",l.textContent=`Restore to @${s.name}`,l.addEventListener("click",m=>{m.stopPropagation(),e.set("restore_request",{tag:s.name}),e.save_changes()}),c.appendChild(l)}i.appendChild(c)}),o.appendChild(i);let a=document.createElement("div");return a.className="pw-node-artifacts",n.annotation.artifacts.forEach(s=>a.appendChild(_(s))),o.appendChild(a),o}function fe(n){let t=document.createElement("div");t.className="pw-target-selector";let e=n.get("selection")?.artifact??{},o=e.artifactId?String(e.artifactId):null;if(!o)return t.classList.add("pw-target-selector-empty"),t.textContent=`No target selected \u2014 use "Pick from notebook" or hover an element's "+".`,t;let r=e.artifactName?String(e.artifactName):o,i=e.granularity?String(e.granularity):"dataset",a=e.detail?String(e.detail):null,s=z[i]??i,c=document.createElement("div");c.className="pw-target-selector-label",c.textContent="Target",t.appendChild(c);let l=document.createElement("div");return l.className="pw-target-selector-value",l.textContent=a?`${r} \xB7 ${s}: ${a}`:`${r} \xB7 ${s}`,l.title=`artifact id: ${o}`,t.appendChild(l),t}function ge(n,t){let e=document.createElement("div");e.className="pw-composer",e.appendChild(fe(t));let o=document.createElement("textarea");o.placeholder="Describe reasoning for this analytical step...",e.appendChild(o);let r=document.createElement("input");r.placeholder="#tags",e.appendChild(r);let i=document.createElement("button");return i.className="pw-commit-btn",i.textContent="Commit annotation",i.addEventListener("click",()=>{let a=t.get("selection")?.artifact??{},s={annotationId:`ann-${Date.now()}`,title:o.value.slice(0,40)||"Untitled annotation",note:o.value,tags:r.value.split(" ").filter(Boolean).map(g=>({name:g.replace(/^#/,""),color:"#64748B"})),artifacts:a.artifactId?[{artifactId:String(a.artifactId),artifactName:String(a.artifactName??""),granularity:String(a.granularity??"dataset"),detail:a.detail??null}]:[],author:{agent_id:"agent-student",agent_type:"human",display_name:"S"},timestamp:new Date().toISOString()},c=t.get("tree"),l=s.artifacts[0]?.artifactId,m=l&&c.nodes[l]?l:"composer",p=t.get("commits");t.set("commits",[...p,{stateId:m,annotation:s}]),t.save_changes(),o.value="",r.value=""}),e.appendChild(i),e}function K(n,t){n.innerHTML="";let e=t.get("tree"),o=t.get("mode");Y=t.get("commits");let r=document.createElement("div");r.className="pw-tree-header";let i=document.createElement("div");i.className="pw-tree-title",i.textContent="Annotation History",r.appendChild(i);let a=document.createElement("div");a.className="pw-tree-caption",a.textContent="Every annotation made in this analysis, oldest first.",r.appendChild(a),n.appendChild(r);let s=document.createElement("div");s.className="pw-commit-rail",me(e).forEach(c=>s.appendChild(ue(c,o,t))),n.appendChild(s),o==="student"&&n.appendChild(ge(n,t))}var X={CLEANING:"#0EA5E9",FEATURE_ENGINEERING:"#10B981",ANALYSIS:"#F59E0B",COMPARISON:"#F43F5E",DATA_LOADING:"#8B5CF6",AGGREGATION:"#8B5CF6"};function Q(n,t={}){let e=document.createElement("div");if(e.className="pw-node-card",t.collapsed&&e.classList.add("pw-node-card-collapsed"),t.selected&&e.classList.add("pw-node-card-selected"),e.dataset.stateId=n.stateId,e.style.setProperty("--category-color",X[n.operation.category]??"#64748B"),t.collapsed){let i=document.createElement("div");return i.className="pw-node-title",i.textContent=n.operation.name,e.appendChild(i),t.onSelect&&(e.style.cursor="pointer",e.title="Click to expand",e.addEventListener("click",t.onSelect)),e}let o=document.createElement("div");o.className="pw-op-badge",o.textContent=n.operation.category.replace("_"," "),e.appendChild(o);let r=document.createElement("div");if(r.className="pw-node-title",r.textContent=n.operation.name,e.appendChild(r),n.annotations.length===0){let i=document.createElement("div");i.className="pw-node-no-annotations",i.textContent="No annotations on this state yet.",e.appendChild(i)}return n.annotations.forEach(i=>{let a=document.createElement("div");a.className="pw-annotation-block";let s=document.createElement("div");s.className="pw-annotation-title",s.textContent=i.title,a.appendChild(s);let c=document.createElement("div");c.className="pw-node-note",c.textContent=i.note,a.appendChild(c);let l=document.createElement("div");l.className="pw-node-tags",i.tags.forEach(p=>l.appendChild(B(p))),a.appendChild(l);let m=document.createElement("div");m.className="pw-node-artifacts",i.artifacts.forEach(p=>m.appendChild(_(p))),a.appendChild(m),e.appendChild(a)}),t.onSelect&&(e.style.cursor="pointer",e.title="Click to expand",e.addEventListener("click",t.onSelect)),e}function he(n,t){let e=[],o=t;for(;o;){let r=n.nodes[o];e.unshift(r),o=r.parentStateId}return e}function V(n,t){n.innerHTML="";let e=t.get("tree"),r=(t.get("selection")??{}).node?.pickedStateId;if(r===void 0){let p=document.createElement("div");p.className="pw-inspect-empty",p.textContent="Click a node in the Tree tab to inspect it here.",n.appendChild(p);return}let i=r,a=e.nodes[i],s=document.createElement("div");s.className="pw-breadcrumb",s.textContent=he(e,i).map(p=>p.operation.commandName).join(" -> "),n.appendChild(s);let c=document.createElement("div");c.className="pw-diff-pane",a.delta.columnsAdded.forEach(p=>{let g=document.createElement("div");g.className="pw-diff-line pw-diff-added",g.textContent=`+ ${p.name}: ${p.dtype}`,c.appendChild(g)}),a.delta.columnsRemoved.forEach(p=>{let g=document.createElement("div");g.className="pw-diff-line pw-diff-removed",g.textContent=`- ${p.name}: ${p.dtype}`,c.appendChild(g)});let l=document.createElement("div");l.className="pw-diff-line",l.textContent=`rows: ${a.delta.rowCountBefore} -> ${a.delta.rowCountAfter}`,c.appendChild(l),n.appendChild(c);let m=document.createElement("div");m.className="pw-annotation-pane",a.annotations.forEach(p=>{let g=document.createElement("div");g.className="pw-node-title",g.textContent=p.title,m.appendChild(g);let y=document.createElement("div");y.className="pw-node-note",y.textContent=p.note,m.appendChild(y);let b=document.createElement("div");b.className="pw-node-tags",p.tags.forEach(v=>b.appendChild(B(v))),m.appendChild(b)}),n.appendChild(m)}function we(n){let t=new Map;for(let e of Object.values(n.nodes))for(let o of e.annotations)for(let r of o.artifacts)t.set(r.artifactId,r.artifactName);return[...t.entries()].map(([e,o])=>({id:e,name:o}))}function ve(n){let t=new Set;for(let e of Object.values(n.nodes))for(let o of e.annotations)for(let r of o.tags)t.add(r.name);return[...t].sort()}function xe(n,t){return n.annotations.some(e=>e.artifacts.some(o=>o.artifactId===t))}function Ee(n,t){return n.annotations.some(e=>e.tags.some(o=>o.name===t))}function Z(n,t){n.innerHTML="";let e=t.get("tree"),o=t.get("commits")??[];for(let{stateId:h,annotation:d}of o){let f=e.nodes[h];f&&(f.annotations.some(x=>x.annotationId===d.annotationId)||(f.annotations=[...f.annotations,d]))}let r=document.createElement("div");r.className="pw-tree-header";let i=document.createElement("div");i.className="pw-tree-title",i.textContent="Provenance Tree",r.appendChild(i);let a=document.createElement("div");a.className="pw-tree-caption",a.textContent="Click a card to drill into its branch and inspect it. Use the filters below to spotlight matching steps \u2014 the rest collapse to a single line.",r.appendChild(a),n.appendChild(r);let s=new Set,c=null,l=document.createElement("div");l.className="pw-filter-bar",n.appendChild(l);let m=document.createElement("div");m.className="pw-filter-row";let p=document.createElement("span");p.className="pw-filter-label",p.textContent="Tags",m.appendChild(p),l.appendChild(m);let g=document.createElement("div");g.className="pw-filter-row";let y=document.createElement("span");y.className="pw-filter-label",y.textContent="Linked artifact",g.appendChild(y),l.appendChild(g);let b=document.createElement("div");b.className="pw-breadcrumb",n.appendChild(b);let v=document.createElement("div");v.className="pw-columns-scroll",n.appendChild(v);let w=document.createElement("div");w.className="pw-columns",v.appendChild(w);let I=document.createElement("div");I.className="pw-tree-inspect-pane";let M=document.createElement("div");M.className="pw-tree-inspect-pane-label",M.textContent="Inspect",I.appendChild(M);let L=document.createElement("div");I.appendChild(L),n.appendChild(I);let C=t;(!C._pwTreePath||C._pwTreePath[0]!==e.rootId)&&(C._pwTreePath=[e.rootId]);let u=C._pwTreePath;function E(h){let d=s.size===0||[...s].some(x=>Ee(h,x)),f=c===null||xe(h,c);return d&&f}function J(){return s.size>0||c!==null}function de(){if((t.get("selection")??{}).node?.pickedStateId===void 0){L.classList.add("pw-tree-inspect-pane-empty"),L.textContent="Click a card above to inspect it here.";return}L.classList.remove("pw-tree-inspect-pane-empty"),V(L,t)}function D(){m.querySelectorAll(".pw-filter-chip").forEach(d=>d.remove());let h=ve(e);if(h.length===0){let d=document.createElement("span");d.className="pw-filter-chip pw-filter-chip-empty",d.textContent="none yet",m.appendChild(d);return}h.forEach(d=>{let f=document.createElement("button");f.className=s.has(d)?"pw-filter-chip pw-filter-chip-active":"pw-filter-chip",f.textContent=`#${d}`,f.addEventListener("click",()=>{s.has(d)?s.delete(d):s.add(d),D(),F()}),m.appendChild(f)})}function G(){g.querySelectorAll(".pw-filter-chip").forEach(d=>d.remove());let h=we(e);if(h.length===0){let d=document.createElement("span");d.className="pw-filter-chip pw-filter-chip-empty",d.textContent="none yet",g.appendChild(d);return}h.forEach(d=>{let f=document.createElement("button");f.className=c===d.id?"pw-filter-chip pw-filter-chip-active":"pw-filter-chip",f.textContent=d.name,f.addEventListener("click",()=>{c=c===d.id?null:d.id,G(),F()}),g.appendChild(f)})}function q(){b.innerHTML="",u.forEach((h,d)=>{let f=e.nodes[h],x=document.createElement("span");if(x.className="pw-breadcrumb-item",x.textContent=f.operation.name,b.appendChild(x),d<u.length-1){let k=document.createElement("span");k.className="pw-breadcrumb-sep",k.textContent=">",b.appendChild(k)}})}function j(h,d){let f=d.get(h);if(f!==void 0)return f;let x=e.nodes[h],k=x.parentStateId?j(x.parentStateId,d)+1:0;return d.set(h,k),k}function pe(h){w.querySelector("svg.pw-tree-lines")?.remove();let d=document.createElementNS("http://www.w3.org/2000/svg","svg");d.setAttribute("class","pw-tree-lines"),d.setAttribute("width",String(w.scrollWidth)),d.setAttribute("height",String(w.scrollHeight));let f=w.getBoundingClientRect();for(let[x,k]of h){let P=e.nodes[x],T=P.parentStateId?h.get(P.parentStateId):void 0;if(!T)continue;let A=T.getBoundingClientRect(),N=k.getBoundingClientRect(),$=A.right-f.left+w.scrollLeft,R=A.top+A.height/2-f.top+w.scrollTop,S=N.left-f.left+w.scrollLeft,H=N.top+N.height/2-f.top+w.scrollTop,W=($+S)/2,O=document.createElementNS("http://www.w3.org/2000/svg","path");O.setAttribute("d",`M ${$} ${R} C ${W} ${R}, ${W} ${H}, ${S} ${H}`),O.setAttribute("class",u.includes(x)&&u.includes(P.parentStateId??"")?"pw-tree-line-active":"pw-tree-line"),d.appendChild(O)}w.insertBefore(d,w.firstChild)}function F(){w.innerHTML="";let d=(t.get("selection")??{}).node?.pickedStateId,f=new Map,x=new Map,k=0;for(let T of Object.values(e.nodes)){let A=j(T.stateId,f);k=Math.max(k,A),(x.get(A)??x.set(A,[]).get(A)).push(T)}let P=new Map;for(let T=0;T<=k;T++){let A=document.createElement("div");A.className="pw-column",(x.get(T)??[]).forEach(N=>{let R=!u.includes(N.stateId)||J()&&!E(N),S=Q(N,{collapsed:R,selected:N.stateId===d,onSelect:()=>{u.splice(T,u.length,N.stateId);let H=t.get("selection")??{};t.set("selection",{...H,node:{pickedStateId:N.stateId}}),t.save_changes(),q(),F()}});P.set(N.stateId,S),A.appendChild(S)}),w.appendChild(A)}pe(P),de()}D(),G(),q(),F()}var be=["student","reviewer"];function ee(n,t){n.innerHTML="";let e=document.createElement("div");e.className="pw-config-heading",e.textContent="Persona",n.appendChild(e);let o=t.get("mode");be.forEach(r=>{let i=document.createElement("label");i.className="pw-config-radio";let a=document.createElement("input");a.type="radio",a.name="pw-persona",a.value=r,a.checked=o===r,a.addEventListener("change",()=>{t.set("mode",r),t.save_changes()}),i.appendChild(a);let s=document.createElement("span");s.textContent=r,i.appendChild(s),n.appendChild(i)})}var ne=[];function oe(n){ne.push(n)}function ae(n,t){for(let e of ne){let o=e.resolve(n,t);if(o)return o}return null}function te(n,t){return t!==null&&n.includes(t)}oe({name:"html-table",resolve(n,t){let e=t.closest("td");if(e&&te(n,e)){let r=e.parentElement,i=r?Array.from(r.children).indexOf(e):-1,a=e.closest("table"),s=null;a&&i>=0&&(s=a.querySelector("thead tr")?.children[i]?.textContent?.trim()??null);let c=r?.parentElement,l=r&&c?Array.from(c.children).indexOf(r):-1;return{granularity:"cell",detail:s?`row ${l}, column ${s}`:`row ${l}`}}let o=t.closest("th");return o&&te(n,o)?{granularity:"column",detail:o.textContent?.trim()??null}:null}});oe({name:"svg-mark",resolve(n){for(let t of n){if(!(t instanceof Element)||t.closest("svg")===null&&t.tagName.toLowerCase()!=="svg")continue;let e=t.getAttribute("data-pw-detail");if(e)return{granularity:t.getAttribute("data-pw-granularity")??"mark",detail:e};let o=t.getAttribute("aria-label");if(o&&t.getAttribute("role")==="graphics-symbol")return{granularity:"mark",detail:o}}return null}});function re(n){let t=n;for(;t;){if(t.dataset&&t.dataset.pwArtifactId)return t;if(t.parentElement)t=t.parentElement;else{let e=t.getRootNode();t=e instanceof ShadowRoot?e.host:null}}return null}function ye(n,t,e){let o=ae(e,t);return o||{granularity:n.dataset.pwGranularity??"dataset",detail:n.dataset.pwDetail??null}}function ie(n,t,e){let o=null;function r(c){let l=re(c.composedPath()[0]??c.target);o&&o!==l&&o.classList.remove("pw-picker-hover"),l&&l.classList.add("pw-picker-hover"),o=l}function i(c){let l=c.composedPath(),m=l[0]??c.target,p=re(m),g=m;if(s(),!p){e?.();return}c.preventDefault(),c.stopPropagation();let{granularity:y,detail:b}=ye(p,g,l);t({artifactId:p.dataset.pwArtifactId,artifactName:p.dataset.pwArtifactName??"",granularity:y,detail:b})}function a(c){c.key==="Escape"&&(s(),e?.())}function s(){document.removeEventListener("mousemove",r,!0),document.removeEventListener("click",i,!0),document.removeEventListener("keydown",a,!0),o&&o.classList.remove("pw-picker-hover")}document.addEventListener("mousemove",r,!0),document.addEventListener("click",i,!0),document.addEventListener("keydown",a,!0)}function se(n){let t=n;for(;t;){if(t.dataset&&t.dataset.pwArtifactId)return t;if(t.parentElement)t=t.parentElement;else{let e=t.getRootNode();t=e instanceof ShadowRoot?e.host:null}}return null}var ce=new WeakSet;function le(n,t){if(ce.has(n))return;ce.add(n);let e=null,o=null;function r(){return e||(e=document.createElement("button"),e.className="pw-gutter-plus",e.textContent="+",e.style.position="fixed",e.style.display="none",e.addEventListener("click",s=>{if(s.preventDefault(),s.stopPropagation(),!o)return;let c=o.dataset.pwGranularity??"dataset",l=o.dataset.pwDetail??null;t({artifactId:o.dataset.pwArtifactId,artifactName:o.dataset.pwArtifactName??"",granularity:c,detail:l}),i()}),document.body.appendChild(e),e)}function i(){e&&(e.style.display="none"),o=null}function a(s){let c=r();o=s;let l=s.getBoundingClientRect();c.style.display="block",c.style.top=`${l.top}px`,c.style.left=`${l.right-20}px`}document.addEventListener("mouseover",s=>{if(n.get("mode")!=="student"||document.body.classList.contains("pw-picking-active")){i();return}let c=se(s.composedPath()[0]??s.target);c&&a(c)}),document.addEventListener("mouseout",s=>{if(!o)return;let c=s.relatedTarget;c&&(o.contains(c)||c===e||e?.contains(c))||se(s.composedPath()[0]??s.target)!==o||i()})}function Ce({model:n,el:t}){t._pwTreePortal?.remove(),t.innerHTML="";let e=document.createElement("div");e.className="pw-root",t.appendChild(e);let o=document.createElement("div");o.className="pw-content",e.appendChild(o);let r=document.createElement("div");o.appendChild(r);let i=document.createElement("div");i.className="pw-restore-banner",i.style.display="none",o.appendChild(i);let a=document.createElement("div");a.className="pw-body",o.appendChild(a);let s=80;new ResizeObserver(u=>{let E=u[0]?.contentRect.width??e.clientWidth;e.classList.toggle("pw-collapsed",E<s),v.style.display!=="none"&&L()}).observe(e);let l=document.createElement("div");l.className="pw-picker-row";let m=document.createElement("button");m.className="pw-pick-btn",m.textContent="Pick from notebook";let p=!1;function g(){p=!1,m.classList.remove("pw-pick-btn-armed"),m.textContent="Pick from notebook",document.body.classList.remove("pw-picking-active")}m.addEventListener("click",()=>{p||(p=!0,m.classList.add("pw-pick-btn-armed"),m.textContent="Picking\u2026 (Esc to cancel)",document.body.classList.add("pw-picking-active"),ie(n,u=>{g();let E=n.get("selection")??{};n.set("selection",{...E,artifact:u}),n.save_changes(),C()},()=>{g()}))}),l.appendChild(m);function y(){r.innerHTML="";let u=n.get("active_tab");r.appendChild(U([{value:"curated",label:"Annotations"},{value:"tree",label:"Tree"},{value:"config",label:"Config"}],u,E=>{n.set("active_tab",E),n.save_changes()}))}function b(){let u=n.get("restore_request");n.get("mode")==="reviewer"&&u&&u.tag?(i.style.display="block",i.textContent=`Restoring parameters to @${u.tag} \u2014 dependent marimo cells will re-run reactively.`):i.style.display="none"}let v=document.createElement("div");v.className="pw-tree-portal",Object.assign(v.style,{display:"none",position:"fixed",top:"0",right:"0",bottom:"0",zIndex:"900",background:matchMedia("(prefers-color-scheme: dark)").matches?"#0F172A":"#F8FAFC",color:matchMedia("(prefers-color-scheme: dark)").matches?"#F8FAFC":"#0F172A",borderLeft:`1px solid ${matchMedia("(prefers-color-scheme: dark)").matches?"#334155":"#E2E8F0"}`,padding:"16px 24px",overflow:"hidden",fontFamily:"ui-sans-serif, system-ui, sans-serif"});let w=document.createElement("div"),I=t.getRootNode();I instanceof ShadowRoot&&(I.adoptedStyleSheets.forEach(u=>{let E=document.createElement("style");E.textContent=[...u.cssRules].map(J=>J.cssText).join(`
2
+ `),w.appendChild(E)}),I.querySelectorAll("style").forEach(u=>{w.appendChild(u.cloneNode(!0))})),v.appendChild(w);let M=document.createElement("div");Object.assign(M.style,{display:"flex",flexDirection:"column",height:"100%",minHeight:"0"}),v.appendChild(M),document.body.appendChild(v),t._pwTreePortal=v;function L(){let u=e.getBoundingClientRect();v.style.left=`${u.right}px`}function C(){let u=n.get("active_tab");if(a.innerHTML="",u==="tree"&&t.offsetParent!==null){L(),v.style.display="block",Z(M,n);return}v.style.display="none",M.innerHTML="";let E=document.createElement("div");if(a.appendChild(E),u==="config"){ee(E,n);return}K(E,n),n.get("mode")==="student"&&a.appendChild(l)}window.addEventListener("resize",()=>{v.style.display!=="none"&&L()}),y(),b(),C(),n.on("change:active_tab",()=>{y(),C()}),n.on("change:mode",()=>{C(),b()}),n.on("change:tree",C),n.on("change:commits",C),n.on("change:restore_request",b),le(n,u=>{n.get("active_tab")!=="curated"&&n.set("active_tab","curated");let E=n.get("selection")??{};n.set("selection",{...E,artifact:u}),n.save_changes(),y(),C(),a.querySelector(".pw-composer textarea")?.focus()})}var et={render:Ce};export{et as default};
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+
5
+ import anywidget
6
+ import traitlets
7
+
8
+ from provenance_widget.interfaces import ProvenanceSource
9
+ from provenance_widget.serialize import tree_to_json
10
+
11
+ _STATIC_DIR = pathlib.Path(__file__).parent / "static"
12
+
13
+
14
+ class ProvenanceWidget(anywidget.AnyWidget):
15
+ _esm = _STATIC_DIR / "widget.js"
16
+ _css = _STATIC_DIR / "widget.css"
17
+
18
+ tree = traitlets.Dict({}).tag(sync=True)
19
+ mode = traitlets.Unicode("student").tag(sync=True)
20
+ active_tab = traitlets.Unicode("curated").tag(sync=True)
21
+ selection = traitlets.Dict({}).tag(sync=True)
22
+ commits = traitlets.List([]).tag(sync=True)
23
+ restore_request = traitlets.Dict({}).tag(sync=True)
24
+ restore_ack = traitlets.Dict({}).tag(sync=True)
25
+
26
+
27
+ class ProvenancePanel:
28
+ """Python-side façade wrapping one ProvenanceWidget instance."""
29
+
30
+ def __init__(self, source: ProvenanceSource, mode: str = "student"):
31
+ self._source = source
32
+ self.widget = ProvenanceWidget(tree=tree_to_json(source.get_tree()), mode=mode)
33
+
34
+ @property
35
+ def source(self) -> ProvenanceSource:
36
+ return self._source
37
+
38
+ def sidebar(self) -> ProvenanceWidget:
39
+ return self.widget
40
+
41
+ def tree_surface(self) -> ProvenanceWidget:
42
+ return self.widget
43
+
44
+ def refresh(self) -> None:
45
+ """Re-pull the tree from the source.
46
+
47
+ For sources whose history grows over time (e.g. a live pmprov
48
+ RuntimeTracker being driven by an in-progress notebook run, as
49
+ opposed to MockProvenanceSource's fixed snapshot), call this after
50
+ new steps have been recorded so the sidebar reflects them.
51
+ """
52
+ self.widget.tree = tree_to_json(self._source.get_tree())
53
+ # Belt-and-braces: under marimo (unlike Jupyter), a Dict trait set
54
+ # from a cell that isn't itself re-running doesn't reliably push a
55
+ # fresh sync message to an already-displayed widget — force one.
56
+ self.widget.send_state(["tree"])
57
+
58
+ def commit_annotation(self, annotation_dict: dict, state_id: str | None = None) -> None:
59
+ """Record an annotation, associating it with an analysis state.
60
+
61
+ Pass state_id explicitly to force it. Otherwise this resolves it
62
+ from the annotation's own targeted artifact(s), via the source's
63
+ `state_for_artifact()` — the state that contains/produced the
64
+ artifact the annotation is actually about. Falls back to "python"
65
+ if no artifact resolves (e.g. a general note not tied to output).
66
+ """
67
+ if state_id is None:
68
+ for ref in annotation_dict.get("artifacts") or []:
69
+ resolved = self._source.state_for_artifact(ref.get("artifactId", ""))
70
+ if resolved:
71
+ state_id = resolved
72
+ break
73
+ if state_id is None:
74
+ state_id = "python"
75
+ self.widget.commits = [*self.widget.commits, {"stateId": state_id, "annotation": annotation_dict}]
76
+
77
+ def request_restore(self, tag: str) -> None:
78
+ self.widget.restore_request = {"tag": tag}
79
+
80
+ def acknowledge_restore(self) -> None:
81
+ self.widget.restore_ack = {"acknowledged": True}
82
+ self.widget.restore_request = {}