Status: planning
Last updated: 2026-05-03

# Study dependencies — planning

How a codestudy declares "I want these other files available before mine runs." Captures the design walked through with Derek on 2026-04-26/27, before any code lands.

For the codestudy system as it stands today, see [`codestudy-architecture-handoff.md`](codestudy-architecture-handoff.md). For the per-module reference, see [`templates/modules/study.md`](../../templates/modules/study.md).

## What the system is for

Codestudies are small teaching surfaces — "show an example, mess around a little, mid-workshop exploration." Students do their real work in their own real codebases, not here. The dependency system has to support deps when they're useful (a sequence of pens building on a shared helper, a mid-program study using PE's CSS baselines), but it has to be **invisible when not used.** Most pens have zero dependencies. The system has to feel weightless until it's needed.

## Use cases the system needs to cover

The design follows from a walk through the actual scenarios — early-course pens through to advanced ones. Most of these resolve to two or three distinct mechanics; the rest is combinations.

### CSS

| # | Case | What's new |
|---|---|---|
| 1 | study with no deps | — (works today) |
| 2 | study + `reset.css` from the codebase | Dep field; CSS stitching |
| 3 | study + N CSS files | Order matters — cascade resolves |

### PHP

| # | Case | What's new |
|---|---|---|
| 1 | study with PHP, no deps | — (works today) |
| 2 | study + a PHP file declaring `$data` (e.g. an associative array) | PHP stitching |
| 3 | study + N PHP files | — (same mechanic) |
| 4 | study + a `pirates.json` file the pen reads with `file_get_contents` | Foreign-format file *placed alongside*, not stitched |
| 5 | study using native `require_once` syntax against another PHP file | Opt-in "real-language imports" mode |

### JS

| # | Case | What's new |
|---|---|---|
| 6 | study with JS, no deps | — (works today) |
| 7 | study + `helpers.js` (defines a function) or `pirates.js` (declares `const pirates`) | JS stitching |
| 8 | study + N JS files | — (same mechanic) |
| 9 | study + a CDN-hosted lib (Vue, etc.) | URLs in the dep field can be external |
| 10 | study using ESM `import { x } from './helpers.js'` | Opt-in "real-language imports" mode |

### Combinations

Cases like "HTML + CSS + PHP + JS, all with deps" are just the union of the above. Each file type has a clear treatment; the combo works without new mechanics.

### Not yet covered, flagged

- **HTML partials** — Derek raised this Sunday night ("we'll have PHP components to show partials I think"). Not in the use-case list yet. Could fall out of the placed-alongside mechanic (an `.html` file the pen `include`s server-side), or it might want a different treatment. **Update 2026-05-13:** a concrete use case named itself — many studies sharing one piece of markup (e.g. a "theme example" article structure used across a half-dozen tokens/CSS pens). Treatment for this case is now scoped under [HTML-as-entry import (next concrete step)](#html-as-entry-import--next-concrete-step) below. Other HTML-partial use cases (server-side `include` of a fragment) remain deferred.

## The model in one sentence

**A study has a list of file URLs; the runner treats each file according to its extension; the pen runs.** No imports, no exports, no namespace, no `@layer`, no graph traversal, no transitive resolution, no page-level setup.

## Two sources for a file

A dep is a URL. URLs come from two places, treated identically by the runner:

| Source | URL pattern | Edited via |
|---|---|---|
| Standalone file in the theme repo | `/study-resources/{filename}` | Editor / git |
| A pane from another Study | `/study/{slug}/{filename}` | Study editor (CMS) |
| External (CDN) | Any URL | External provider |

Most use cases live in `/study-resources/`. The Study-pane source matters when a file deserves CMS editing (curriculum baselines, things students should be able to read as artifacts). The CDN source falls out for free.

The Study-pane URL is a small WP route that reads `study_data_json` and serves the matching file as plain text with the correct mime type.

## How files are treated, by extension

| Extension | Treatment | Notes |
|---|---|---|
| `.css` | Stitched into the document's stylesheet area | Cascade resolves order |
| `.js` | Stitched as a `<script>` before the pen's JS | Globals available to the pen |
| `.php` | Concatenated server-side before the pen's PHP | Functions/`$vars` available to the pen |
| `.json` / `.txt` / `.csv` / `.html` | Placed alongside — file sits in the working directory at a same-origin URL | Pen reads it with `file_get_contents` (PHP) or `fetch` (JS). `.html` may also gain an "entry document" treatment — see [HTML-as-entry import](#html-as-entry-import--next-concrete-step) below. |
| `.png` / `.svg` / `.jpg` / fonts / etc. | Placed alongside | Pen references by URL |

**Stitched** = file content is folded into the document/request. Pen "inherits" what the file declared.

**Placed-alongside** = file is reachable at a same-origin URL or as a sibling on disk. Pen reads it with normal language idioms (no magic). Sidesteps the cross-origin fetch headache because everything is same-origin within the runner's iframe sandbox.

## Two writing modes for stitched code: magic vs. native

Most pens use the **magic mode** — files get stitched, their declarations just exist in scope. Author writes nothing about how it got there. Right for the bulk of teaching surfaces.

A pen can opt into a **native mode** when the lesson IS about modules:

- **PHP native:** files placed at predictable paths, pen uses `require_once 'helpers.php'`. Real PHP idiom.
- **JS native (ESM):** pen's main script runs as `<script type="module">`, files reachable via `import { x } from './helpers.js'`. Real ES modules.

Same architectural pattern, different language. A flag on the Study (`native_mode: 'php' | 'js' | 'both' | null`) controls it. Default is null = magic mode.

We don't need to build this in v1 — magic mode covers all early-curriculum cases. But the design has to leave room for it; cases 5 and 10 in the use-case lists *will* come up when teaching modules.

## The data: per-study `imports`

`imports` lives in **`study_data_json` postmeta**, not as an ACF field. Reasoning: the codestudy architecture splits Study fields by axis — identity (title, description, desktop_only, goal, featured) lives in ACF; *content* (files, zones, editor flags) lives in `study_data_json` written by the front-end editor. Imports are content, not identity — they're "what makes this pen tick," edited next to the files. Same save path, no new ACF surface.

Updated JSON shape:

```json
{
	"editable": true,
	"lineNumbers": false,
	"showGrid": false,
	"files": [...],
	"zones": { "left": [...], "right": ["output"], "bottom": [...] },
	"imports": [
		"/study-resources/reset.css",
		"/study-resources/setup.css",
		"/study-resources/pe-colors.css",
		"/study/pirate-data/pirates.js",
		"/study-resources/pirates.json"
	]
}
```

Order in the array = stitching order for stitched files. First entry runs first. Placed-alongside files don't have a meaningful order, but they share the field — one list, simple model.

For v1 the editor exposes it as a textarea (one URL per line) or a small repeater. A "browse and pick" affordance can come later.

## The UI: the imports popover

Visible to everyone (not admin-gated). Shown only when imports is non-empty — early-course pens with no deps show no chrome at all.

**Current shape (2026-05-03): per-file `[@]` chip in the file pane header.**

The first pass put a global "Imports (n)" strip above all the panes. It was distracting — always-on chrome competing with the actual content. Replaced with a small `[@]` button anchored to the right side of the file pane's header (the same row the filename sits in). Click opens a popover listing the import filenames; each one links to the raw file in a new tab.

- **Trigger:** `[@]` button in the file header. Renders only on HTML / PHP files (the entry-point document — imports get stitched into its `<head>`). CSS / JS files don't get a chip.
- **Subtle by default:** opacity 0.6 at rest; lifts to 1 on hover / focus / when open. The `@` symbol is read as "imports" by anyone who's seen `@import`, npm scopes, or ES modules.
- **Layout trick:** the chip uses `padding: 0.4rem; margin-block: -0.4rem` so its touch area exists but its layout height collapses — the file header is sized by the filename, not by the chip.
- **Group wrapper:** `.code-study-panel-actions` holds the chip(s). Future per-file actions (lock, edit, etc.) become siblings inside the same wrapper.
- **Stacking:** the file header is `position: relative; z-index: 2`, which lifts it above CodeMirror's internal layers (cursor layer is z-index 150 in the editor's stacking context) so the popover renders over the editor instead of behind it.
- **Accessibility:** `aria-label='Imports'` + `title='Imports'` on the chip; `aria-expanded` reflects state; popover is `role='dialog'` with a visible "Imports" heading; Esc closes; click-outside closes; `:focus-visible` outline kept since the resting state has no border.

**Content (current):** flat list of filenames. **Not yet grouped by treatment** (the original "Stitched / Placed alongside" grouping below) — the data is currently a flat array of URLs and the runner already knows how to treat each by extension. Grouping is an "if it earns its place" upgrade once we have studies that mix stitched and placed-alongside files in ways that confuse readers.

```
Imports
  reset.css
  setup.css
  pe-colors.css
```

**Originally designed shape (deferred):** vertical list grouped by treatment.

```
Stitched
  reset.css
  pssst-baseline.css
  tokens.css
  helpers.php

Placed alongside
  pirates.json
```

Adding type indicators (`CSS` / `JS` / `PHP`) or "what it exposes" annotations is also "if it earns its place."

**Source files for the chip + popover:**

- [`code-study/src/components/CodeStudy.vue`](https://github.com/...) — chip, popover, click-outside / Esc handling, injection of imports from `study-context`.
- [`code-study/src/components/StudyFrame.vue`](https://github.com/...) — provides `imports` on the shared `study-context`. The previously-rendered global "Imports (n)" strip has been removed; the edit-mode authoring textarea is unchanged.

## Authoring

**Always opt-in. Auto-created Studies have zero dependencies.**

The dependency list is itself teaching material — a student opening any study should see exactly what's available, no hidden defaults. That only works if there are no hidden defaults. Pens are raw by default.

To soften authoring friction:

- **"Add the PE standard stack"** button in the Study editor. One click writes `[/study-resources/reset.css, /study-resources/pssst-baseline.css, /study-resources/tokens.css]` into the field.
- Possibly named presets later (`Standard`, `Branded` (= standard + voice + palette), `Raw`). Probably overkill for v1.

The standard-stack files (`reset.css`, `pssst-baseline.css`, `tokens.css`) live in `/study-resources/` as plain files in the theme repo. They're hand-edited like any other CSS. They're also reachable as raw URLs, so they double as readable artifacts: a student can click through from the popover and read the reset on its own.

## What we deliberately did NOT build, and why

These all came up in the design conversation and were rejected — reasons recorded so the next person doesn't re-litigate them.

- **`@layer` wrapping for CSS deps.** The cascade already does what we need. `@layer` was solving a problem we don't have.
- **`PE.*` namespace for JS data.** Just declare the variable in a JS file. No parsing, no namespace. Same as how external resources work on CodePen.
- **Cross-origin `fetch` from the iframe.** Sidestepped by the placed-alongside mechanic — files are same-origin within the runner's iframe sandbox, so `fetch` works without CORS gymnastics.
- **Slotted layers** (`reset_layer`, `tokens_layer`, etc.). Flat list is enough. Slots can be reintroduced later as an *authoring affordance* (presets, named buttons) without changing the schema.
- **Page-level setup module.** A 4-pen page that all share `pirates.js` would be the use case. Decided: list it on each pen. A few extra clicks once in a while is cheaper than a new module type with accumulate-while-walking-the-page semantics.
- **Transitive dep resolution.** A pen's `imports` list is exactly what gets stitched/placed. If you import `helpers.js` from Study B, you do *not* automatically pick up B's own deps. Stitching is dumb. No graph, no cycles.
- **Body marker (`showBodyMarker`) for early-course pens.** Considered as a training-wheels affordance. Rejected — students should learn the document tree, not have it pointed out.

## Deferred — likely worth building, just not now

- **Native-language imports mode** (cases PHP-5 and JS-10/ESM in the use-case lists). Magic mode covers v1. Build native mode when the curriculum reaches modules; it's one feature with two language implementations.
- **"Duplicate this study"** authoring action. Surfaced by the Alina paint-calculator case (six pens forming one progressive story). Already noted in [`codestudy-architecture-handoff.md`](codestudy-architecture-handoff.md#deferred-after-codepen-migration) under "Duplication coupling."
- **Snapshot vs. drift.** If a page references a featured Study or `/study-resources/` file and that file is later rewritten, the page changes with it. Default is "accept drift." A per-placement snapshot flag would freeze content at save time. Already flagged in [`templates/modules/study.md`](../../templates/modules/study.md#library-vs-drift-deferred). Not pressing until cross-pen reuse is real.
- **Browse / pick affordance for adding a dep.** v1 is paste-a-URL. A picker that lists `/study-resources/*` and known featured Studies would be friendlier once the library has more than a handful of entries.
- **HTML partials.** Not in the use-case list yet. Likely falls out of placed-alongside (`.html` files included by the pen's PHP) but defer the design until a real use case names itself.

## HTML-as-entry import — next concrete step

**Added 2026-05-13.** First real cross-pen reuse case has named itself: a "Theme example" article structure (`<article class='thing'>` with header / main / footer) used identically across multiple CSS/tokens studies. Each pen wants the same markup; only the CSS differs.

**Decision: solve via `/study-resources/` + a new "entry document" treatment on `.html` imports, NOT via the deferred cross-Study URL route (#8).** The shared markup is developer-edited, not CMS-editable, and doesn't need to exist as a viewable study in its own right. `/study-resources/` is already the home for shared developer-edited assets; this is one more file in that folder.

### What "entry document" means

Today the pen's own `index.html` (a file in `files[]`) is the document the runner builds output from. Pen-authored CSS and JS layer onto that document.

The new treatment: when `imports` contains an `.html` URL, **that file becomes the document.** The pen's local CSS and JS layer onto the imported markup. The pen may have no local `index.html` at all.

### Contract

| Aspect | Behavior |
|---|---|
| **How it's authored** | `imports: ['/study-resources/theme-example.html']` |
| **Pen has no local `index.html`** | Imported HTML is the document. CSS/JS imports + pen files stitch in normally. |
| **Pen has a local `index.html`** | TBD — see "Open question" below. Most likely: imported entry wins, local `index.html` becomes a no-op (or warns). |
| **Multiple `.html` imports** | First one wins as entry; the rest are placed-alongside (status quo). Or: forbid multiple, error at save. Lean toward the latter. |
| **Pane visibility** | Imported HTML has no pane (it's not a local file). This naturally gives the "shared markup, no pane to hide" outcome — falls out for free. |

### Why this doesn't paint us into a corner

The treatment is additive — `.html` keeps its placed-alongside behavior for other cases. The "entry document" semantics activate only when the runner sees an `.html` import AND the pen wants it as the document. If we later add a more powerful model (per-file flags, explicit `entry` field), this implicit treatment can stay as the v1 shorthand or be migrated.

### Open questions

- **Implicit vs. explicit entry.** Implicit: any `.html` in `imports` becomes the entry document. Explicit: a separate field, e.g. `entry: '/study-resources/theme-example.html'`. Implicit is magical and breaks if a future case needs multiple HTML imports with non-entry semantics. Explicit is one more concept but unambiguous. **Leaning explicit** — but defer the final call to the implementation moment.
- **Local `index.html` collision.** If the pen has both an imported entry and a local `index.html`, who wins? Cleanest: forbid the combination at save time (the imports popover warns). The author chooses one or the other.
- **CodeMirror file list.** Should the imported HTML show up in the imports popover only, or also as a read-only entry in the editor's file list (so students can click through to see what they're inheriting)? Popover-only is simpler; the URL is clickable already.

### Locked contract (2026-05-13, before any code)

Captured here so the system can grow studies against a stable shape — re-doing 20 studies to migrate an implicit-vs-explicit decision later is the failure mode this section exists to prevent.

| Decision | Locked |
|---|---|
| **Field name & shape** | `entry: string \| null` in `study_data_json`, sibling to `imports`. Empty/null = use local `index.html` (today's behavior). Filled = use this URL. |
| **Implicit vs. explicit** | **Explicit.** `entry` is its own field, not "first .html in imports wins." Other `.html` files in `imports[]` stay placed-alongside; their semantics are unchanged. |
| **Local `index.html` collision** | **Forbid at save.** If `entry` is set and `files[]` contains `index.html`, save handler returns an error: "Remove local index.html, or clear the entry import." Author picks one. No silent precedence. |
| **Fragment vs. full document** | Runner sniffs for `<html>`. Has `<html>` → use as-is, pen CSS appended to `<head>`, pen JS appended to `<body>`. No `<html>` → wrap with standard runner template, fragment goes in body. Keeps shared markup files readable (just `<article>…</article>`, no wrapper boilerplate). |
| **Relative URLs in entry HTML** | Inject `<base href='{origin}/'>` into the srcdoc so relative URLs (`<img src='foo.png'>`) resolve against the entry's origin, not `about:srcdoc`. Same fix family as the existing imports system's absolutization. |
| **Client-fetch pattern (PHP pens)** | Same as existing imports: client fetches `entry`, sends content to the PHP runner in the payload. Runner stays dumb about cross-origin. |
| **Editor UI for entry** | Single text field labeled "Entry HTML (URL, optional)" above the existing Imports textarea in the edit toolbar. Read-only chip in the FILES list showing the entry filename, greyed, `↗` link to source, no `×`. |
| **Broken entry URL** | Save allows it (don't block on fetch). Editor chip shows a small error indicator. Runner produces blank output. Author finds out by testing. |

### Implementation order

1. **Data shape** — `study_data_json` gains `entry: string \| null`. Default null. No migration of existing studies needed (null = today's behavior).
2. **Save handler validation** — reject save when `entry` set AND `files[]` has `index.html`. Both the in-page `study_module` save and the standalone `/study/{slug}/?edit=true` save.
3. **Editor UI** — text field above the Imports textarea (`StudyFrame.vue` edit toolbar). Read-only chip in the FILES list. Wire `getEntry()` / `setEntry()` through `defineExpose` like the existing imports flow.
4. **Client-side runner (`core/output.js` → `buildSrcdoc`)** — accept `entry` option. If set, fetch it, sniff for `<html>`, build srcdoc accordingly. Inject `<base href>`. Skip building from `files[].find(.html)`.
5. **PHP runner** — `code-study/php-runner.php` accepts an `entry` field. Pen's PHP layers on as today. Deploy via the separate `code-study` repo + production runner path.
6. **`/study-resources/theme-example.html`** — the actual first file. Just the `<article class='thing'>` markup from the screenshots, no wrapper.
7. **First study uses it** — pick one of the existing tokens/CSS studies, set `entry: '/study-resources/theme-example.html'`, remove its local `index.html`, save, verify.

Steps 1–3 are theme-side; step 4 is the front-end build; step 5 is the separate runner repo + deploy; steps 6–7 are content. Each step lands without breaking what's there.

### Editor UI placement (for reference)

```
FILES:                PANES:        PRESENTATION:    ENTRY HTML (optional):
[index.html ×]        ☑ output      ☑ Editable       [/study-resources/theme-example.html]
[style.css ×]         ☐ console     ☐ Line Numbers
[theme-example.html ↗] (greyed)     ☐ Show Grid      IMPORTS (one URL per line):
[+ Add File]                        + Add PE         [/study-resources/reset.css      ]
                                      standard stack [                                ]
```

The greyed entry chip in FILES is the visible signal "this study is using a shared document." Click `↗` to read it.

### What this unlocks today

- The Theme example case: write `theme-example.html` once in `/study-resources/`, import it from N studies, edit it via git, all consumers update.
- The "hidden pane" case for shared markup — solved by virtue of imports having no panes. No new pane-hiding feature needed yet.

### What this does NOT unlock (and is fine deferring)

- Studies sharing markup that needs CMS editing — still wants the cross-Study URL route (#8).
- Per-placement presentation (showing the same pen with different panes in different contexts) — that's the larger direction below.
- Authoring a study with files that are local-but-hidden — still requires the file/pane split.

---

## Direction: files vs. panes vs. placement (bridge, not plan)

**Added 2026-05-13. This is direction, not a committed plan.** The HTML-as-entry step above is a bridge toward a larger architectural split this section sketches. Capturing it here so the next concrete step doesn't accidentally close off the larger one. Nothing here is scheduled; nothing here is decided.

### The split

Today the Study object conflates three concerns:

1. **Content** — files, imports, what runs.
2. **Presentation** — zones, which panes show, editable / lineNumbers / showGrid / size.
3. **Placement** — implicit; the page-module embeds a study and inherits its presentation 1:1.

The direction is to separate these into three layers:

| Layer | What it owns | Where it lives |
|---|---|---|
| **Files** | Content + order. Pure data. | `study_data_json.files` (today, unchanged) |
| **Default panes** | Which files / runtime panes show, in what zones, with what editor flags. The "study at rest." | A new `presentation` block on the study (extracted from today's flat `study_data_json`) |
| **Placement override** | Same shape as default panes. Optional. Empty = inherit default. | ACF fields on the `study_module` page-module instance |

Resolution at render time: placement override → study default → system default. First match wins; empty fields fall through.

### Why this earns its place (eventually)

CodePen got away with one fixed pane model (HTML / CSS / JS / output) because every pen looks the same. PE's content is heterogeneous (HTML-only, CSS+HTML, PHP+data, multi-file projects, code-reference studies) AND its teaching uses are heterogeneous (live editor, output preview, code-only reference, hidden helper files). Two axes of variation → the split pays off.

Cases the split would handle cleanly:
- **Same study, different presentations per placement.** "Absolute Positioning" embedded in workshop A with full editor; embedded in workshop B as output-only preview. One source, two surfaces.
- **Hidden files.** Files that the runner needs but the author doesn't want students to see. Today's only workaround is to push the file out to `/study-resources/`, which forces non-shared content into a shared location.
- **Display-only embeds** (like the Image #1 "Theme methodologies" case — code shown without a runner). Becomes a named presentation, not a separate component.
- **Reusing one pen's files in another pen.** Once files are addressable independent of their pane, the cross-Study URL route (#8) is a small extension, not a redesign.

### The authoring UX (sketch, not committed)

The risk is exposing the split as raw config (zones, visible[], editable…). That's terrible for authors. The model:

- **Study editor surface:** "Add file" creates content. "Add pane" picks a file (or output / console / browser / response) and drops it onto the layout. Panes can be dragged off the layout into an "on-deck" tray — file still runs, pane just isn't shown. Same drag mechanic for file panes and runtime panes; no checkbox/drag asymmetry.
- **Page-module surface:** the placement editor shows the study's default layout, prefilled. Author rearranges → saved as the override. If left alone, no override is saved.
- **Author shortcut (page-module):** for the 95% case, a small "Mode" dropdown — Default / Output only / Code reference / Custom… — covers most overrides without exposing the schema. Custom opens the full layout editor.

### Why we're not building this now

- No teaching surface forces it. The current shared-markup case is solvable via the HTML-as-entry step alone.
- The data migration is small but non-trivial — `study_data_json` would gain a nested `presentation` block, and the front-end editor + render path both need to read the new shape.
- Authoring UX for the page-module override needs design work that hasn't been scoped.

### What we're keeping on the table by writing this down

- When the file/pane coupling next gets in the way, the answer is "lift presentation out of files," not "add another flag."
- When cross-Study file reuse comes up (item #8), the route should serve *files*, not "the whole study minus the panes" — i.e. the design should match the file-as-first-class model.
- When the page-module gains any presentation knob, it should be shaped as an override on a presentation block, not as ad-hoc properties on the embed.

Related: cross-Study URL route ([#8 in "What's deferred (unfinished today)"](#whats-deferred-unfinished-today)); HTML-as-entry import (section above).

---

## Operations notes

### `/study-resources/` on production (nginx)

SpinupWP's nginx config has a static-file location block that handles `*.css`, `*.js`, etc. with `try_files $uri =404` — meaning these requests never fall back to PHP. The `init`-priority-1 hook in `services/study/resources.php` works locally (where Apache routes everything through `index.php`) but does NOT fire on live.

**The fix:** a docroot symlink that lets nginx serve the file directly without any PHP involvement.

```bash
ssh perpetualAdmin@165.232.138.219
cd /sites/perpetual.education/files
ln -sfn wp-content/themes/perpetual-2019/study-resources study-resources
```

Set up 2026-04-27. The docroot at `/sites/perpetual.education/files/` is not itself a git repo, so the symlink simply persists as a filesystem entry. **If a future server rebuild or filesystem reset removes it, recreate with the command above.** The PHP route handler stays in place as the local-dev fallback.

## Open questions

- **The exact URL pattern for Study-pane files.** Going with `/study/{slug}/{filename}` (shortest, cleanest). If it collides with WP routing or `is_singular('study')` template handling, fall back to `/study/{slug}/files/{filename}`.
- **PSSST.** Used as a placeholder name throughout this doc. Confirm where the curriculum-side definition of PSSST lives (or write it down) before baking the slug `pssst-baseline.css` into the standard-stack button.
- **Runner contract.** Today `peprojects.dev/api/php/run.php` accepts a single pen's files. The minimum change is "accept an ordered list of dep file URLs; treat each by extension." Spec'ing that contract is the first concrete implementation step.

## Implementation status (2026-04-27)

Built today, working end-to-end for client-side pens (HTML/CSS/JS):

1. ✓ `/study-resources/` folder + canonical stub files (`reset.css`, `setup.css`, `pe-colors.css`).
2. ✓ PHP read path. `render-module.php` reads `imports` from `study_data_json`, passes through `templates/modules/study.php` and `templates/components/codestudy.php` to the Vue mount as a prop. No ACF field; `imports` is part of the JSON content.
3. ✓ Vue popover. `<code-study>` accepts an `imports` prop. Originally rendered a `<details>` strip ("Imports (N)") above all panes; replaced 2026-05-03 with a per-file `[@]` chip in the file header (HTML/PHP files only) that opens a popover listing the imports. See "The UI: the imports popover" section above for the rationale and current shape.
4. ✓ `code-study.iife.js` rebuilt and deployed into the theme.
5. ✓ Editor UI. The Vue edit toolbar (admin-only, edit mode) has an Imports textarea (one URL per line) and an "Add PE standard stack" button that wires up the three canonical files. `getImports()` / `setImports()` exposed via `defineExpose`. Both save handlers (in-page Save in `study_module`, standalone `/study/{slug}/?edit=true`) include `imports` in the payload — same save path that already handles `study_data_json`.
6. ✓ Client-side srcdoc stitching. `buildSrcdoc` in `core/output.js` now accepts an `imports` option; CSS imports become `<link rel="stylesheet" href="…">` tags in `<head>` (cascade order: pen wins by default), JS imports become `<script src="…"></script>` tags in `<body>` before the inline pen JS. URLs are absolutized against the parent origin so the iframe srcdoc resolves them correctly.

End-to-end test: open `/study/{slug}/?edit=true`, click "Add PE standard stack," save, reload — popover shows three entries, output iframe applies `* { margin: 0 }` and `* { box-sizing: border-box }` and `--pe-blue: blue` from the linked CSS. Pen-authored CSS overrides cleanly.

## What's deferred (unfinished today)

7. **PHP runner imports — code complete, deploy pending.** As of 2026-04-27 evening:
   - **Vue side** ([`code-study/src/components/StudyOutput.vue`](https://github.com/...)) now resolves imports client-side before posting to the runner. CSS/JS imports are skipped (handled in srcdoc), PHP/data imports are fetched as text and sent to the runner as `{ name, content, ext }` entries. The runner stays dumb — no cross-origin fetching from peprojects.dev to perpetual.education needed.
   - **Production runner** ([`code-study/php-runner.php`](https://github.com/...)) accepts the new `imports` array. PHP imports are concatenated before the entry's code (with leading `<?php` stripped) so functions/vars are in scope for the pen's PHP. Data files (json/csv/txt/html) are stashed in `__runner_files` so `include 'pirates.json';` works; direct `file_get_contents` reads remain blocked by the safety check.
   - **Local runner** ([`api/study.php`](../../api/study.php)) has the same logic for parity — useful as a reference impl.
   - **Built artifact** (`scripts/code-study.iife.js`) rebuilt and copied into the theme.

   **Remaining deploy steps:**
   - Push the theme changes through the normal Tower → SpinupWP path (all the local edits above ride along).
   - Deploy the updated `php-runner.php` to `peprojects.dev/api/php/run.php`. That's a separate operation handled outside this repo.

   Until the runner deploys, PHP pens with imports work visually (popover, save) but the deps don't stitch in the actual execution.

8. **`/study/{slug}/{filename}` raw-file route.** Not built; until cross-Study deps are needed, `/study-resources/` is the only file source. Add when the first real cross-Study reuse case appears.

9. **Native-imports opt-in mode** for PHP `require_once` and JS ESM `import`. Defer until a workshop is *teaching* modules.

10. **"Duplicate this study"** authoring action. Surfaced by the Alina paint-calculator case (a sequence of pens that are largely copy-modify of each other). Already noted in [`codestudy-architecture-handoff.md`](codestudy-architecture-handoff.md#deferred-after-codepen-migration) under "Duplication coupling."

11. **Snapshot vs. drift** per-placement flag. Not pressing until cross-pen reuse is real.

12. **Browse / pick affordance** for adding a dep — picker that lists known Studies + `/study-resources/`. v1 is paste-a-URL; upgrade when the library has more than a handful of entries.

13. ~~**Imports matrix / fixtures page.**~~ ✓ done 2026-04-27. Lives at `/design-system/modules/study-imports` ([`templates/design-system/modules/study-imports.php`](../../templates/design-system/modules/study-imports.php), routed via [`functions/design-system-routes.php`](../../functions/design-system-routes.php)). Five variants of the same pen — raw, +reset, +reset+setup, +full stack, pe-colors-only — each annotated with what should change visually. Use it to verify cascade behavior, opt-out semantics, and that each layer does what it claims.

## Files touched today

| File | What |
|---|---|
| `study-resources/reset.css`, `setup.css`, `pe-colors.css`, `README.md` | Stub canonical files + folder readme |
| `render-module.php` | Read `imports` from JSON, pass through |
| `templates/modules/study.php` | Forward `imports` to component + include in save payload |
| `templates/components/codestudy.php` | Accept `imports`, JSON-encode, pass to Vue mount |
| `templates/pages/study-detail.php` | Read `imports`, forward, include in standalone save payload |
| `templates/modules/study.md` | Document `imports` in JSON shape |
| `code-study/src/components/StudyFrame.vue` | `imports` prop, local state, setter, popover, edit toolbar UI, standard-stack button, expose getters |
| `code-study/src/components/StudyOutput.vue` | `imports` prop, pass to `buildSrcdoc` |
| `code-study/src/components/StudyPane.vue` | Pass `imports` from context to StudyOutput |
| `code-study/src/core/output.js` | `buildSrcdoc` emits `<link>` and `<script src>` for CSS/JS imports |
| `code-study/src/components/StudyOutput.vue` | Resolves imports client-side, sends to runner for PHP pens |
| `code-study/php-runner.php` (production runner) | Accepts `imports` array, prepends PHP imports to entry code, stashes data files in runner_files |
| `api/study.php` (`pe_run_php`) | Same imports logic for the local runner — reference impl + dev parity |
| `scripts/code-study.iife.js`, `styles/code-study.css` | Rebuilt + deployed |
| `services/study/resources.php` (new), `functions.php` | Route handler for `/study-resources/{filename}` (local Apache) |
| Live: docroot symlink `study-resources` → theme folder | Same URL works on nginx without PHP involvement |
| `templates/design-system/modules/study-imports.php` (new), `functions/design-system-routes.php` | Imports matrix page at `/design-system/modules/study-imports` |
| `docs/version-3/codestudy-architecture-handoff.md` | Pointer to this doc under "Deferred" |
| `docs/version-3/study-dependencies-planning.md` | This doc |
