Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 48 additions & 24 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,18 @@ jobs:
retention-days: 5
if-no-files-found: error

integration:
name: PR / Linux integration
integration_shards:
name: PR / Linux integration shard ${{ matrix.shard }}
needs: build
# Keep the required check red when preflight/build fails or is skipped.
if: always()
# Fail closed on upstream failure, but let superseded runs be cancelled.
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
# Measured test workload is ~39m; build time has its own job budget.
# Disjoint execution preserves every assertion and the per-job time budget.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
env:
WGPU_BACKEND: vulkan
FIGGY_TEST_JUPYTER: '1'
Expand All @@ -113,6 +117,7 @@ jobs:
FIGGY_REQUIRE_INTERACTION_PARITY: '1'
FIGGYPLOT_MATURIN_ACTION: .ci/maturin-action/action.yml
PYTHONUNBUFFERED: '1'
CI_SHARD: ${{ matrix.shard }}
steps:
- name: Require the verified wheel
env:
Expand Down Expand Up @@ -151,40 +156,59 @@ jobs:
run: |
.venv/bin/python -m playwright install --with-deps chromium
.venv/bin/python scripts/ci_gpu_env.py
- name: Test previous failures first, then native and Jupyter integration
- name: Execute disjoint native and Jupyter test shard
run: |
mkdir -p ci-results
xvfb-run -a -s '-screen 0 1600x1000x24' bash <<'SCRIPT'
set -euo pipefail
openbox > /tmp/figgyplot-openbox.log 2>&1 &
wm_pid=$!
trap 'kill "$wm_pid" 2>/dev/null || true' EXIT
# Reject the observed stale-font/box failures before the full suite.
# The queue-order cases must fail on the original a1e79a8 wheel.
.venv/bin/python -m pytest -vv --capture=tee-sys --durations=20 \
--basetemp=ci-results/layout-probe --junitxml=ci-results/layout-probe.xml \
'tests/test_interaction_parity.py::test_inline_editor_resize_uses_current_units_and_applies_real_edits[1]' \
'tests/test_interaction_parity.py::test_inline_editor_resize_uses_current_units_and_applies_real_edits[2]' \
'tests/test_interaction_parity.py::test_inline_text_resize_multiline_and_existing_format_in_both_hosts[cartesian]' \
'tests/test_interaction_parity.py::test_inactive_overlay_objects_can_be_edited_and_dragged_directly[native]' \
2>&1 | tee ci-results/layout-probe.log
# Measure the last failed radial screenshot before the full suite.
# Await current surface completion before the unchanged capture deadline.
.venv/bin/python -m pytest -vv --capture=tee-sys --durations=20 \
--basetemp=ci-results/priority --junitxml=ci-results/priority.xml \
tests/test_packaged_assets.py \
'tests/test_interaction_parity.py::test_annotation_drawing_editing_and_cancel_in_both_hosts[radial]' \
2>&1 | tee ci-results/priority.log
.venv/bin/python -m pytest -vv --capture=tee-sys --durations=30 \
# Collect the entire suite; execute each node ID on one runner.
# The required integration job audits exact JUnit execution IDs.
.venv/bin/python scripts/run_ci_tests.py -x -vv --capture=tee-sys --durations=30 \
--ci-shard-count=4 --ci-shard-index="$CI_SHARD" \
--basetemp=ci-results/full --junitxml=ci-results/junit.xml \
2>&1 | tee ci-results/full.log
SCRIPT
- name: Save integration results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: pr-linux-integration-results
name: pr-linux-integration-shard-${{ matrix.shard }}
path: ci-results
retention-days: 5
if-no-files-found: ignore


integration:
name: PR / Linux integration
needs: integration_shards
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every shard to succeed
env:
SHARD_RESULT: ${{ needs.integration_shards.result }}
run: test "$SHARD_RESULT" = success
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: pr-linux-integration-shard-*
path: ci-results
- name: Verify complete passing execution without duplicates or skips
run: |
for shard in 0 1 2 3; do
mv "ci-results/pr-linux-integration-shard-$shard" "ci-results/shard-$shard"
done
python3 scripts/check_ci_execution.py ci-results --shards=4
- name: Save execution audit
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: pr-linux-integration-audit
path: ci-results/execution-audit.json
retention-days: 5
if-no-files-found: error
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ in [Editor commands and history](docs/EDITOR_COMMANDS.md).

## Edit once, reuse across datasets

For an optional multi-Figure agent review workflow, use
[`figgyplot.review.Session`](docs/REVIEW_MODE.md) and the
[review controller example](examples/review_workflow.py). Review comments are
separate from chart annotations. Approval checkpoints exact SSOT edits and binary
source data before closing the view; the complete next round is published after
source execution and rendering validation. The ordinary editor needs no session.

Run the example shown in the GIF:

```bash
Expand Down Expand Up @@ -432,7 +439,8 @@ rates are reported separately; they are not monitor FPS measurements.
The native extension uses the published **figgy-renderer 0.18.1** and
**figgy-model 0.12.1** crates from crates.io, pinned in `Cargo.lock`.
The packaged browser kernel is built from their upstream release commit
`3a0a6a3` with a recorded web-binding patch for non-borrowing GPU completion.
`f3f569c4c0e32d3d0682632444b6fe7ce84cba8d` with a recorded web-binding patch
for non-borrowing GPU completion and current annotation hit geometry.
Asset provenance checks pair the registry versions and checksums with that
source revision and verify the binding patch hash. Rust owns chart layout,
rendering, picking and drag policies; this repository adds the NumPy-facing API,
Expand Down
10 changes: 9 additions & 1 deletion docs/HOSTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

The figgyplot 0.3.0 source uses the published `figgy-renderer 0.18.1` and
`figgy-model 0.12.1` crates from crates.io. `Cargo.lock` pins their versions
and checksums; packaged WASM uses their release commit `3a0a6a3`.
and checksums; packaged WASM uses their release commit
`f3f569c4c0e32d3d0682632444b6fe7ce84cba8d`.
Provenance checks preserve this source pairing; see [publishing](PUBLISHING.md).
Chart layout, styles, data
rendering, hit maps and drag policies remain renderer APIs. No renderer source is
Expand Down Expand Up @@ -96,6 +97,13 @@ presentation or completed GPU execution. `benchmarks/ui_surface.py` measures
surface submission rate; the existing `compare.py` measures completed RGBA
readback. Do not compare the two as the same FPS metric.

Both native chart families also report `pointer_sequence` and `pointer_input`
for the last cursor-motion or mouse-button event included in a submitted surface frame. The event
records its kind, physical client coordinates, button/state and egui's consumed
flag. These diagnose delivered input independently of chart SSOT or selection;
a surface-frame increment alone does not identify which input was processed.
The UI harness retains bounded before/after input records and timestamps.

## Jupyter

Native and Jupyter WebGPU hosts support Cartesian, radial, categorical and boxplot
Expand Down
136 changes: 136 additions & 0 deletions docs/REVIEW_MODE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Review sessions (protocol 1)

Status: unreleased implementation for #18, pending API review. Use a wheel built
from this branch; the previously published figgyplot 0.3.0 does not include it.
See the [validation record](REVIEW_MODE_VALIDATION.md) for measured test coverage.

Issue #18 adds an explicit `figgyplot.review.Session` lifecycle. Ordinary
`Figure`, `show`, notebook editing and Copy Python do not create review files,
start agents or require an LLM runtime.

## API and lifecycle

```python
from figgyplot.review import Session

session = Session(source={"entrypoint": "plots.py", "function": "build", "arguments": {}})
fig = session.figure(id="signal")
fig.axes[0].plot([0, 1], [1, 3], id="measurement")
session.present() # native windows; use session.widget(fig) in a notebook
session.run_events() # handles review requests while pumping native windows
```

Native and notebook review panels contain a dedicated **Review comments** input,
**Request changes**, and **Accept**. Chart text/annotations stay chart data.
Approval flushes input, checks the displayed revision, publishes durable feedback,
then closes the approved view. Failed publication keeps the view editable with
the comment intact. Closing a window manually leaves its disposition pending.
`session.submit(figure_id, disposition, comments=..., expected_revision=...)`
is the synchronous native/headless interface; `await session.submit_async(...)`
first acknowledges notebook edits. `skip` is an explicit programmatic disposition.
`session.start()` seals a complete set for headless use without opening views.
`session.comment(figure_id, text, target=...)` appends a targeted API comment.
The editable UI comment has its own channel; changing or clearing it preserves
API comments and their targets. Both channels are included in feedback.
Runtime revision counters are scoped to recorded Figure instance IDs. Recovery
creates a new instance; baseline and approved instance/revision pairs therefore
remain explicit. Persistent hashes and stable IDs identify the compared states.
The session round number, rather than a process-local Figure counter, drives
successor publication and cleanup.

Figure IDs use the existing document identity namespace. Choose a Figure ID
different from its panel IDs (the default panel is `main`).
Review mode supports one active interactive view per Figure in a round. A second
native window is rejected; duplicate notebook views must be closed before
approval, so another view's pending input cannot be discarded. Native PNG
fallback editors must also be closed before review approval. Ordinary manual
Figures retain their existing multiple-view behavior.

All required Figures form one sealed review set. Only when all have an explicit
disposition can `session.events()` emit the batch's stable work ID and manifest
location. Repeated polling returns the same pending work. Accepting every Figure
without edits/comments finishes the round without requesting code changes.
Source writes belong to one agent/staging workspace, outside the UI event loop.

`Session.open(cache_dir)` resumes a session. `restore(figure_id)` reconstructs a
Figure from the last complete settings and binary payloads. It does not depend
on the original Python process, GPU resources, notebook transfer buffers or an
unbounded undo journal. `cancel()` preserves the last recoverable round.

`regenerate(work_id, superseded=...)` loads the recorded source entry point,
calls its `build(session, **arguments)` function, validates every candidate by
rendering, and publishes a complete new round. Build/render failure preserves
the pending work and previous checkpoint. The source hash must stay unchanged
during validation. Stable Figure IDs must remain present unless explicitly
superseded. Human property changes must be reproduced by source, or explicitly
superseded with a reason. Merely acknowledging an operation is insufficient.
The next complete set is shown only after publication. Agent integration can
use the CLI and filesystem protocol without importing private Python objects.
Source imports, recorded working directory and rendering execute in an isolated
Python worker process, with a configurable timeout. They cannot change the
controller's working directory, module state or UI event loop.

## Feedback and persistence

The versioned session manifest carries session/round/work IDs, source entry point,
function, arguments and digest, per-Figure schema/renderer versions, baseline and
approved revisions, full baseline/approved settings, canonical persistent hashes,
exact edit operations, separately entered comments, and binary references.
Runtime hover/selection is removed using the same Rust projection as edit history.
Operations use JSON Pointer paths, before/after existence and values, and stable
identity selectors. Object creation, removal and ordering are explicit operations;
there is no guessed gesture delta. The baseline and approved snapshots remain
available to interpret derived layout/link changes.

Numeric input is cached at upload time as content-addressed NumPy binary files
(`allow_pickle=False`), with dtype, shape, byte size and SHA-256. Matrix rows and
vector/mapping columns retain their source IDs; bounded-family values reside in
the complete settings document. Settings-only revisions reuse immutable files.
No permanent native CPU array copy or GPU readback is introduced.

Create sessions in a dedicated empty directory, or omit `cache_dir` to allocate
one. `Session.open` requires an existing manifest. Cache creation rejects
nonempty foreign directories before creating files or running cleanup.

Writers and readers hold a process/file lock. Payloads are flushed before a
temporary manifest is flushed and atomically replaced; the directory is flushed
where supported. Only the published manifest defines a complete revision.
Interrupted writes cannot replace the previous complete state. Garbage collection
runs after commits and on reopen/access. It retains the newest complete round,
unfinished work and every referenced baseline/data file. Higher complete rounds
allow reclamation of superseded completed rounds in the same session lineage.
Readers and active writers are protected by the same lock. Cleanup never depends
on an agent acknowledgement or compares unrelated Figure revision counters.

The filesystem adapter is local and agent-neutral. A disconnected agent resumes
by polling `python -m figgyplot.review events CACHE`; raw arrays never enter the
feedback text. `feedback CACHE WORK` reads structured feedback. Before editing
source, `claim CACHE WORK --worker ID` obtains a renewable writer lease;
`regenerate CACHE WORK --claim-token TOKEN` validates updated source and publishes
its next round. Expired/stale leases cannot publish; duplicate completion returns
the existing successor. `status CACHE` reports pending Figures and rerun errors.
The bundled companion skill documents protocol/version discovery and this loop.
Jupyter controllers use `await session.watch()` to display published successors;
native controllers use `run_events(on_status=...)` to observe progress while
their approved windows are closed.

The [bundled skill](../python/figgyplot/skills/figgyplot-review/SKILL.md) is also
available through `python -m figgyplot.review skill`. Agent hosts use their
existing terminal/tool runner to poll events while a human reviews the figures.
The adapter does not require a vendor model SDK. A disconnected agent resumes
the same durable work ID instead of relying on an in-memory notification.

Run adapter commands on the rendering host through the agent's execution tool.
Remote agents need an explicit execution bridge with access to the cache,
recorded source paths and Python environment. A manifest location alone does not
connect a remote chat service. The validation worker needs a native GPU or a
software GPU runtime, including for notebook-built candidates; the notebook
controller itself continues to use its GPU-free document model.

The cache preserves registered renderer columns and canonical bounded values.
For histograms and boxes these include counts/edges or computed statistics;
original samples, external computation inputs and random seeds belong to the
source builder's reproducible inputs/arguments. The checkpoint restores the
chart without inferring those computations from its rendered result.

![One of two validated next-round Figures in the live browser review controller](images/review-mode-browser.png)
Loading