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
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/048-tab-autocomplete"
"feature_directory": "specs/049-jupyterlite-demo"
}
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ For additional context about technologies to be used, project structure,
shell commands, and other important information, read the spec at
`spec.md` and the constitution at `.specify/memory/constitution.md`.
Active per-item plans live under `specs/<NNN-slug>/plan.md`.
Current active plan: `specs/049-jupyterlite-demo/plan.md`.
<!-- SPECKIT END -->

# Spec-Driven Development on a Project Board
Expand Down
39 changes: 39 additions & 0 deletions specs/049-jupyterlite-demo/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Specification Quality Checklist: JupyterLite Demo Page

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-07-04
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- Items marked incomplete require spec updates before `/speckit-clarify` or `/speckit-plan`.
- The spec deliberately keeps hosting path (e.g. `/jupyterlite/`) and the
specific build toolchain as implementation details for `/speckit-plan`.
- Constitution VI (pinning) and Principle IV (browser-only) are satisfied by
FR-008 / FR-010 and the Assumptions; the JupyterLite toolchain pins are flagged
as a planning-stage constitution amendment rather than a spec decision.
54 changes: 54 additions & 0 deletions specs/049-jupyterlite-demo/contracts/build-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Contract: Reproducible Build Command

Satisfies FR-006 ("reproducible via a single documented command") and FR-007
(additive workflow step).

## Single documented command (local + CI)

From the repository root, with a pinned Python environment:

```bash
pip install -r jupyterlite/requirements.txt \
&& jupyter lite build \
--config jupyterlite/jupyter_lite_config.json \
--output-dir jupyterlite/_output
```

`jupyter_lite_config.json` fixes the content root (`jupyterlite/content/`) and
output location; the base URL is supplied per environment (see below). This
one command is documented verbatim in `jupyterlite/README.md`.

## Base URL parameterisation

JupyterLite bakes its base URL at build time (analogous to the IDE's
`VITE_BASE_PATH`). The publish path differs by environment:

| Environment | Base URL |
|-------------|----------|
| Deploy (`main`) | `/tabular-data-playground/jupyterlite/` |
| PR preview | `/tabular-data-playground/pr-preview/pr-<N>/jupyterlite/` |

The base URL is passed via the JupyterLite config / CLI flag at build time in
each workflow; local builds may use the default/root base for quick checks.

## Workflow integration (additive)

In **`.github/workflows/deploy.yml`** and **`.github/workflows/pr-preview.yml`**,
after the existing IDE build + copy and before the peaceiris publish:

1. `actions/setup-python@<pinned>` (Python 3.11+).
2. `pip install -r jupyterlite/requirements.txt`.
3. `jupyter lite build` with the environment's base URL.
4. `cp -r jupyterlite/_output/. _site/jupyterlite/` (after
`mkdir -p _site/jupyterlite`).

## Invariants

- **C1**: The command exits 0 and produces a non-empty output dir on a clean
checkout (CI guard).
- **C2**: The step does **not** invoke pnpm, does not touch `app/`, and does
not alter the existing IDE/`slides`/`sample-package` assembly. It only adds
`_site/jupyterlite/`.
- **C3**: All Action versions and Python deps are pinned (Constitution VI).
- **C4**: Re-running on the same commit is idempotent (same inputs → same
published tree, modulo timestamps).
40 changes: 40 additions & 0 deletions specs/049-jupyterlite-demo/contracts/site-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Contract: Published Site URL Surface

The demo adds one new path to the published `gh-pages` tree. The existing
surface is unchanged.

## Deploy (push to `main`) — served under `/tabular-data-playground/`

| Path | Serves | Status |
|------|--------|--------|
| `/` | Static welcome page (`web/index.html`) | EXISTING — **edited**: gains a labelled link to `/jupyterlite/` |
| `/slides/` | reveal.js findings deck | EXISTING — unchanged |
| `/playground/` | Frozen Vite IDE (`app/dist/`) | EXISTING — **unchanged** (frozen; FR-008) |
| `/sample-package/` | Lesson 8 Data Package | EXISTING — unchanged |
| `/jupyterlite/` | **NEW** — JupyterLite demo site (`jupyter lite build` output) | NEW |

## PR preview — served under `/pr-preview/pr-<N>/`

The same relative surface is reproduced per PR, so the demo is reviewable
before merge:

- `/pr-preview/pr-<N>/` → welcome (with the link)
- `/pr-preview/pr-<N>/slides/`
- `/pr-preview/pr-<N>/playground/`
- `/pr-preview/pr-<N>/jupyterlite/` → **NEW** demo

## Guarantees

- **G1 (discoverability, FR-004)**: The welcome page at `/` (and per-PR
equivalent) contains a visible, labelled hyperlink whose target resolves to
the `/jupyterlite/` demo under the same base.
- **G2 (headline, FR-002/FR-003)**: Opening `/jupyterlite/`, opening
`demo.ipynb`, and running all cells renders ≥1 matplotlib figure inline, in
the browser, with no server call beyond static-asset + CDN wheel fetches.
- **G3 (cold load, FR-005)**: G2 holds on a first uncached visit in current
Chrome and Firefox.
- **G4 (isolation, FR-008)**: The `/playground/` bytes and the frozen pins are
identical before and after this change; `keep_files: true` and PR-preview
behaviour are preserved.
- **G5 (no leak, FR-009)**: Nothing under `/jupyterlite/` contains
project-specific or sensitive data.
66 changes: 66 additions & 0 deletions specs/049-jupyterlite-demo/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Phase 1 Data Model: JupyterLite Demo Page

This feature is a static demo, not a data-processing system. There is no
persistent server-side data and no runtime schema. The "entities" below are
the shipped build artefacts and their relationships.

## Entities

### JupyterLite demo site

- **What it is**: The static site emitted by `jupyter lite build`, published at
`/tabular-data-playground/jupyterlite/` (deploy) or the PR-scoped preview
path.
- **Composed of**: The JupyterLite app shell + the Pyodide kernel assets +
the ingested `content/` (notebooks + data).
- **Source of truth**: `jupyterlite/` directory (config + requirements +
content). Output is derived and not committed.
- **Validation / invariants**:
- Built output directory (`_output/`) is non-empty after a successful build.
- Base URL baked at build time matches the publish path (deploy vs preview).
- Contains no project-specific or sensitive data (FR-009).
- Independent of the frozen playground's assets and pins (FR-008).

### Example notebook (`demo.ipynb`)

- **What it is**: The shipped notebook a visitor opens and runs.
- **Fields / structure**: An ordered list of cells that (1) optionally install
matplotlib in-browser, (2) load `data/sample.csv`, (3) produce ≥1 matplotlib
figure rendered inline.
- **Relationships**: Reads the Sample tabular dataset; runs on the Pyodide
kernel; lives inside the JupyterLite demo site's `content/`.
- **Validation / invariants**:
- Run-All produces at least one inline matplotlib figure (FR-002, FR-003).
- Uses only the pinned kernel + explicitly installed packages.

### Sample tabular dataset (`data/sample.csv`)

- **What it is**: A generic, light CSV used as plotting input.
- **Fields / structure**: A small number of columns × a few dozen rows;
neutral/public content.
- **Relationships**: Consumed by `demo.ipynb`.
- **Validation / invariants**:
- Generic and shareable; no sensitive or project-specific data (FR-009).
- Small enough to keep payload light (research R6).

### Pin set (`jupyterlite/requirements.txt` + README rows)

- **What it is**: The exact-version pins for the demo toolchain.
- **Fields**: `jupyterlite-core==X.Y.Z`, `jupyterlite-pyodide-kernel==A.B.C`,
plus recorded Pyodide (carried by the kernel) and matplotlib versions in the
README table.
- **Relationships**: Independent of the frozen playground's Pyodide `0.27.7`.
- **Validation / invariants**:
- All pins are exact (`==`), no ranges (Constitution VI).
- Every pin is recorded in `README.md` (gate 5).

## State transitions

None at runtime. The only lifecycle is the build/publish pipeline:

`jupyterlite/ source` → `jupyter lite build` → `_output/` → copied to
`_site/jupyterlite/` → published to `gh-pages` → served at `/jupyterlite/`.

Visitor-side, the notebook has ordinary Jupyter cell execution state, held
entirely in the browser (JupyterLite IndexedDB); it never reaches a server and
is out of scope for persistence.
139 changes: 139 additions & 0 deletions specs/049-jupyterlite-demo/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Implementation Plan: JupyterLite Demo Page

**Branch**: `claude/speckit-issue-21-hjjn3n` | **Date**: 2026-07-04 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/049-jupyterlite-demo/spec.md`

## Summary

Publish a self-contained JupyterLite site to `/jupyterlite/` on the existing
GitHub Pages tree. It ships one example notebook that loads a light, generic
sample CSV and renders a matplotlib figure inline — all in the browser, no
server, no accounts. The build (`jupyter lite build`) runs as an **additive**
step in the existing deploy and PR-preview workflows and copies its output
into the assembled `_site/jupyterlite/`, leaving the frozen playground's
build (`/playground/`) and pins entirely untouched. The demo carries its own
Python pins in a dedicated `jupyterlite/` source directory. Because
JupyterLite is a **new infrastructural dependency**, this feature carries a
required **constitution amendment** (Technology Constraints) and updates to
`README.md` pins and `docs/limitations.md` — all landed in the same change as
the implementation.

## Technical Context

**Language/Version**: Python 3.11+ (build-time only, for `jupyter lite build`); notebooks run against the JupyterLite Pyodide kernel's bundled Python in-browser. No change to the app's TypeScript/React stack.
**Primary Dependencies**: `jupyterlite-core` (pinned), `jupyterlite-pyodide-kernel` (pinned); matplotlib provided in-browser by the Pyodide kernel / `piplite`. Build orchestrated by `jupyter lite build`.
**Storage**: None. Static files only. Notebooks and sample data are shipped read-only; visitor edits live in the browser (JupyterLite's own IndexedDB), never persisted server-side.
**Testing**: Manual cold-load verification in Chrome + Firefox (run-all-cells → figure renders). Optional Playwright Chromium smoke against the built `/jupyterlite/` page (open notebook, run, assert an `<img>`/canvas figure appears). CI must confirm the build produces a non-empty `_output/`.
**Target Platform**: Static GitHub Pages (`gh-pages` branch); WASM/Pyodide in current Chrome & Firefox.
**Project Type**: Additive static sub-site alongside an existing single-page app; independent build pipeline.
**Performance Goals**: Not a hot path. Set expectations for cold-load: first uncached visit fetches Pyodide + matplotlib wheels from CDN (tens of MB); subsequent loads are browser-cached. No hard latency budget beyond "loads and runs on a cold visit."
**Constraints**: Online-only by design (runtime CDN fetch; no offline/air-gapped path). Must not modify, rebuild, or share the frozen `/playground/` runtime or pins. Generic/shareable sample data only.
**Scale/Scope**: One JupyterLite site, one (optionally two) example notebook(s), one small sample CSV. Single visitor per browser session; no shared state (Constitution Principle IV).

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-checked after Phase 1 design.*

Per-feature gates (constitution "Development Workflow & Quality Gates"):

1. **Research-first** — PASS. The demo evaluates a complementary slice of the
in-browser Python/plotting story (Pyodide + matplotlib via JupyterLite) and
is a legitimate research surface, not product scope creep. It is explicitly
additive and disposable relative to the frozen playground.
2. **Notes-section** — N/A. This is not a curriculum lesson template, so the
Notes & Observations requirement does not apply. (Observations about
JupyterLite/matplotlib-under-Pyodide still land in `docs/limitations.md`.)
3. **Destruction** — PASS. No flow overwrites or deletes user content. Visitor
edits are sandboxed in JupyterLite's own browser storage; nothing touches
the playground's `/workspace` or IndexedDB. No modal confirmation needed.
4. **Backend** — PASS. No backend, account, or telemetry. All execution is
in-browser; the only network use is fetching pinned static assets and
CDN-hosted Pyodide/wheels, which Principle IV expressly permits.
5. **Pinning** — CONDITIONAL. The feature introduces new external
dependencies (`jupyterlite-core`, `jupyterlite-pyodide-kernel`, the
in-browser matplotlib/Pyodide the kernel pulls). They MUST land pinned and
recorded in `README.md`. **Additionally**, the constitution's Technology
Constraints class this as *adding a new infrastructural dependency*, which
"IS an amendment." → tracked in Complexity Tracking; a constitution
amendment (Technology Constraints + Sync Impact Report + version bump) is
part of the implementation change.
6. **Limitations** — CONDITIONAL. The demo introduces new sharp edges
(online-only, no air-gapped path; cold-load wheel weight; separate pin set
from the frozen playground). `docs/limitations.md` MUST be updated in the
same change. Satisfied by FR-010 and a planned limitations entry.

**Gate verdict**: No hard violation. Two conditional gates (Pinning as an
amendment, Limitations) are honoured by scoping the constitution amendment,
README pins, and limitations entry into the implementation. See Complexity
Tracking.

Post-Phase-1 re-check: design keeps the demo in its own `jupyterlite/` source
tree with its own pins and an additive workflow step; the frozen playground's
build and pins are unchanged. Gates still hold.

## Project Structure

### Documentation (this feature)

```text
specs/049-jupyterlite-demo/
├── plan.md # This file (/speckit-plan output)
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output (URL surface + build-command contract)
│ ├── site-surface.md
│ └── build-command.md
├── checklists/
│ └── requirements.md # From /speckit-specify
└── tasks.md # /speckit-tasks output (NOT created here)
```

### Source Code (repository root)

```text
jupyterlite/ # NEW — self-contained demo source, own pins
├── requirements.txt # Pinned: jupyterlite-core, jupyterlite-pyodide-kernel
├── jupyter_lite_config.json # Build config (output dir, base URL /jupyterlite/)
├── jupyter-lite.json # Runtime config (kernel, disabled features)
├── README.md # One documented build command (FR-006)
└── content/ # Shipped into the JupyterLite site
├── demo.ipynb # Example notebook: load CSV → matplotlib figure
└── data/
└── sample.csv # Generic, light sample tabular dataset

web/
└── index.html # EDIT — add visible link to /jupyterlite/ (FR-004)

.github/workflows/
├── deploy.yml # EDIT — additive: build JupyterLite, copy to _site/jupyterlite/
└── pr-preview.yml # EDIT — same additive step for PR previews

README.md # EDIT — record JupyterLite/kernel/matplotlib pins
docs/limitations.md # EDIT — online-only + cold-load + separate-pins entry
.specify/memory/constitution.md # EDIT — Technology Constraints amendment (+ Sync Impact, version bump)
```

**Structure Decision**: A new top-level `jupyterlite/` directory holds the
demo's source with its **own** `requirements.txt` pins, fully isolated from
`app/` (the frozen playground). The two published surfaces — `/playground/`
(Vite/pnpm) and `/jupyterlite/` (`jupyter lite build`) — are built by
independent toolchains and only meet in the workflows' "assemble `_site`"
step, where the JupyterLite `_output/` is copied to `_site/jupyterlite/`
after the existing IDE copy. This mirrors how `/slides/` and `/playground/`
already coexist and preserves `keep_files: true` / PR-preview behaviour.

## Complexity Tracking

> Filled because Constitution Check surfaced an amendment-class dependency.

| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|--------------------------------------|
| New infrastructural dependency (`jupyterlite-core` + `jupyterlite-pyodide-kernel` + a Python build step in CI) — per Technology Constraints this "IS an amendment" | The issue's headline value is an in-browser JupyterLite notebook rendering matplotlib; JupyterLite is the named vehicle and cannot be delivered with the existing Vite/pnpm stack alone | Reusing the frozen playground's Pyodide worker to fake a notebook UI would (a) modify the frozen runtime (forbidden by the issue and Principle VI) and (b) not be a real JupyterLite/notebook surface the issue asks for |
| Second pin set (demo pins independent of the frozen playground's Pyodide `0.27.7`) | The demo must not share or shift the frozen contract; its Pyodide comes from the pinned kernel, decoupled on purpose | A single shared pin set would couple the demo to the freeze and risk the "must not modify the frozen playground" constraint |

**Amendment obligation**: implementation MUST include a constitution
amendment adding a "JupyterLite demo" entry under Technology Constraints
(with Sync Impact Report + MINOR version bump), the README pin rows, and the
`docs/limitations.md` entry — all in the same PR as the code, per the
constitution's own rule and gates 5–6.
Loading
Loading