ID9 — VibeScope Inspector Module Extraction Plan (r204 precursor)

**Operator directive (2026-08-30)**: Prepare a module-extraction plan for the VibeScope inspector so it can mount as a native PM2 panel instead of an iframe. Identify drift in §2/§5/§6/§7 and produce a 2–3 day refactor outline.

**Discovery-only.** No implementation. No pact edits. No extraction.

---

TL;DR

The VibeScope Inspector already has the **right structure** to mount as a PM2 panel — it lives at `studio/modules/vibescope/src/inspector/` (688 LoC, 5 sub-modes, READ-ONLY invariant) with a public API barrel (`index.ts`), frozen types, and a stub `useInspectorSource` hook ready for live data.

The **gap** is mounting: today the Inspector mounts as a FreshCards `<Card>` (`InspectorCard.tsx`, 278 LoC) inside the VibeScope workspace. It does NOT have:

1. A standalone panel root component (host-able from PM2 / any chrome)

2. A subscription model for workspace events

3. An asset-binding input contract

4. A capability manifest for Edge Panel contextual behaviour

5. A StudioContext adapter (it imports `useVSILSource` from `../state/VSILStore` which is module-scoped, not Studio-scoped)

The 2-3 day refactor: **8 steps, ~14-18 hours**, additive-only, no pact edits, no iframe. The output is `studio/modules/vibescope/src/inspector/` exposing a single `<VSILInspectorPanel>` host component that PM2 can mount in 5 lines.

**VPS location reference** (per operator feedback 2026-08-30): **a787** at https://artifacts.freshvibeapps.com/vibecoder-standalone/mockups/vibecoder-vps-location-2026-08-30/index.html

---

1. Files in vibescope that need extraction

Already extractable (public API exists)

```

/workspace/freshvibestudio/studio/modules/vibescope/src/inspector/

├── index.ts 30 LoC — barrel (the public API)

├── types.ts 72 LoC — frozen types (READ-ONLY invariant)

├── useInspectorSource.ts 132 LoC — data hook (STUB today)

├── ASTMode.tsx 87 LoC — sub-mode

├── PropsMode.tsx 85 LoC — sub-mode

├── StateMode.tsx 82 LoC — sub-mode

├── LogsMode.tsx 136 LoC — sub-mode

├── MetadataMode.tsx 64 LoC — sub-mode

└── README.md — doctrine

```

**This is the extraction candidate.** 688 LoC total. No iframe dependency. No `window.*` globals. No PM2-specific imports.

Need extraction (currently embedded in InspectorCard.tsx)

```

/workspace/freshvibestudio/studio/modules/vibescope/src/cards/InspectorCard.tsx 278 LoC

```

The 278 LoC in `InspectorCard.tsx` is the **panel-level glue** (chip row, layout, useVSILSource + parseVSIL + buildSceneGraph + extractInspectorSource pipeline). This is what we need to extract as `<VSILInspectorPanel>`.

Adjacent files (already extractable, no work needed)

```

/workspace/freshvibestudio/studio/modules/vibescope/src/vsil/

├── inspectorExtract.ts 271 LoC — already a pure function

├── parser.ts 879 LoC

├── sceneGraph.ts 323 LoC

└── ... (10 more files, 4720 LoC total)

```

**The `src/vsil/` directory is already a clean importable module.** No extraction work needed. The `inspectorExtract.ts` function is the canonical "VSIL source → Inspector data" pipeline.

Files NOT to extract (out of scope for this refactor)

---

2. Inspector sub-modes — what must become importable

The 5 sub-modes are ALREADY importable via the `index.ts` barrel. Each takes the same `InspectorSource` prop and renders read-only. Confirmed by `wc -l`:

| Sub-mode | LoC | Public | READ-ONLY |

|----------|-----|--------|-----------|

| `ASTMode` | 87 | ✓ | ✓ (no `onClick` / `onChange`) |

| `PropsMode` | 85 | ✓ | ✓ (Phase 7: will add two-way binding to vsilOps — NOT Inspector) |

| `StateMode` | 82 | ✓ | ✓ |

| `LogsMode` | 136 | ✓ | ✓ |

| `MetadataMode` | 64 | ✓ | ✓ |

**No work needed here.** The 5 modes are already a clean importable surface.

---

3. How the panel will subscribe to workspace events

Current state

`InspectorCard.tsx` uses `useVSILSource()` from `../state/VSILStore` — a module-scoped React hook. This means the Inspector can ONLY mount inside the VibeScope workspace (where the VSILStore is set up). It cannot mount in any other panel host.

Target state

The `<VSILInspectorPanel>` takes `source: VSILSource` as a prop, NOT via the store hook. This is the canonical "panel as pure component" pattern. Workspace events are received via:

1. **A pub/sub event bus** — the panel subscribes to `vibescope:source-changed`, `vibescope:selection-changed`, `vibescope:asset-bound` events. Event payloads contain the new `VSILSource` (already-parsed). The bus is a 1-file `EventTarget` (the browser-native primitive, no deps).

2. **A `useVSILInspectorEvents` hook** — wraps the subscription + adds React re-render. Pattern:

```ts

function useVSILInspectorEvents(bus: EventTarget): InspectorSource {

const [source, setSource] = useState<InspectorSource>(...);

useEffect(() => {

const onChange = (e) => setSource(e.detail.source);

bus.addEventListener('vsil:source-changed', onChange);

return () => bus.removeEventListener('vsil:source-changed', onChange);

}, [bus]);

return source;

}

```

3. **The VSILStore is updated to emit events** on every state change (1-line addition: `bus.dispatchEvent(new CustomEvent('vsil:source-changed', { detail: { source } }))` after `setState`).

Drift check vs §5 (workspace events doctrine)

The current VibeScope workspace emits events via `addEventListener('vibechat:toggle-remit', ...)` style — that pattern is consistent. The Inspector will use the **same** `EventTarget` bus, just with VibeScope-prefixed event names.

---

4. How the panel will accept asset-binding input

Current state

`useInspectorSource.ts` is a stub: returns a synthetic mock `InspectorSource`. No way to bind a real asset.

Target state

The `<VSILInspectorPanel>` accepts asset-binding as **props**, not via a hook:

```ts

interface VSILInspectorPanelProps {

/** The VSIL source to inspect. Either inline or loaded from URL. */

source?: VSILSource;

/** URL to load VSIL source from. The panel fetches on mount. */

sourceUrl?: string;

/** Asset binding — which VSIL asset (layer / keyframe / scene) is selected. */

selectedAssetId?: string;

/** Override the active sub-mode. */

initialSubMode?: InspectorSubMode;

/** Read-only override (the Inspector is ALWAYS read-only, but a host can disable deep-link to source). */

allowDeepLink?: boolean;

/** Slot for custom 5th sub-mode. */

customSubMode?: { id: string; label: string; component: React.ComponentType<...> };

}

```

**Key design**: `source` is the preferred binding (zero-cost prop pass), `sourceUrl` is for the "inspect this URL" use case (Edge Panel + asset). The hook `useInspectorSource(props)` handles both.

What the bus emits

When the VibeScope workspace binds an asset (e.g. user clicks a layer in the Visual Card), the workspace fires `vibescope:asset-bound` with `{ assetId, source }`. The Inspector panel subscribes and re-renders.

---

5. How the panel will expose capabilities for contextual Edge Panel behaviour

Capability manifest

A 1-file `capabilities.ts` that exports the panel's introspection API. Edge Panel (or any chrome) reads it to decide what tiles to show:

```ts

export const VSIL_INSPECTOR_CAPABILITIES = Object.freeze({

/** What this panel can do. */

can: Object.freeze({

inspectSource: true,

inspectAsset: true,

deepLinkToSource: true,

exportSnapshot: false, // not in v1

editInPlace: false, // constitutional — Inspector is READ-ONLY

}),

/** What this panel needs. */

needs: Object.freeze({

source: 'required | optional | none',

selection: 'optional',

assetBinding: 'optional',

}),

/** When to show this panel (Edge Panel context rules). */

contextRules: Object.freeze({

showInWorkspace: ['vibescope'], // canonical

showInContext: ['vsil-asset-selected', 'vsil-source-changed', 'inspector-asked'],

showInMode: ['normal', 'half', 'full'], // per peek expansion modes

}),

/** Events the panel listens to. */

listens: ['vibescope:source-changed', 'vibescope:asset-bound', 'vibescope:selection-changed'],

/** Events the panel emits (read-only signals). */

emits: ['vsil-inspector:deep-link', 'vsil-inspector:source-copied'],

});

```

Edge Panel reads this and shows tiles like "Open Inspector for current asset" or "Copy source path" based on the current context.

Type contract

```ts

export type InspectorCapabilities = typeof VSIL_INSPECTOR_CAPABILITIES;

```

A host can `import { VSIL_INSPECTOR_CAPABILITIES } from '.../inspector/capabilities'` and use it without instantiating the panel. **This is the contract that makes the panel context-aware without being context-coupled.**

---

6. StudioContext adapter requirements

Current state

The InspectorCard imports `useVSILSource` from the module-scoped VSILStore. This is **module-local**, not Studio-context-aware.

Target state

**No StudioContext adapter needed for v1.** The `<VSILInspectorPanel>` takes `source` as a prop. The host (PM2 / Edge Panel / any chrome) is responsible for providing the source.

Rationale: a panel that needs a Studio-wide context would be **chrome-coupled**, defeating the point of extraction. The contract is: "I'm a pure read-only viewer of `VSILSource`. You give me the source, I render it."

What COULD be a future adapter (out of scope)

If we later want the Inspector to be auto-bound to the **active workspace's current source** (i.e. just drop `<VSILInspectorPanel />` and it works), we could add a thin `useInspectorSourceFromStudio()` hook that reads from `StudioContext`. This is 20 lines and an additive change, NOT in v1 scope.

---

7. Drift identified in §2/§5/§6/§7

§2 (Inspector sub-modes) — NO DRIFT

All 5 sub-modes are READ-ONLY, frozen types, no `onClick`/`onChange`/`onInput`/`onSubmit`/`onBlur` handlers. The README and types.ts explicitly cite `pact/platform/inspector/inspector.md` and the constitutional invariant. ✅

§5 (workspace events) — DRIFT

The current InspectorCard uses `useVSILSource()` (module-scoped store hook), NOT a workspace event subscription. The pattern is consistent with the rest of VibeScope (which also uses VSILStore directly), but it's not "subscribe to events" in the formal sense. **This is a refactor, not a fix** — the target state adds a real event-bus subscription layer on top.

**Severity**: low. No behavioural difference. Just a different shape.

§6 (asset-binding) — DRIFT

The current `useInspectorSource.ts` is a **stub** (returns a synthetic mock). The `InspectorCard.tsx` does the real work (line 47-54: `useVSILSource()` → `parseVSIL` → `buildSceneGraph` → `extractInspectorSource`). This is OK for VibeScope (single workspace) but **broken for any other host** (PM2, Edge Panel, VibeCoder).

**Severity**: medium. The Inspector is not actually a "panel" today — it's a workspace-card.

§7 (Inspector capabilities) — DRIFT

**No capabilities manifest exists.** Edge Panel has no way to introspect what the Inspector can do. The Inspector is mounted via ad-hoc chip-row in the InspectorCard.tsx, not via a registry/capability system.

**Severity**: medium. Edge Panel contextual behaviour is not currently possible for the Inspector.

Summary of drift

| Section | Drift | Severity | Fix in v1 |

|---------|-------|----------|-----------|

| §2 sub-modes | none | — | — |

| §5 events | module-scoped store instead of event bus | low | yes |

| §6 asset-binding | stub hook + workspace-card glue | medium | yes |

| §7 capabilities | no manifest | medium | yes |

All drift is in the `cards/InspectorCard.tsx` glue layer (278 LoC), NOT in the `src/inspector/` proper (688 LoC). The inspector proper is clean.

---

8. The 8-step refactor (2-3 days, 14-18 hours)

| # | Step | Time | What | Risk |

|---|------|------|------|------|

| 1 | Create `VSILInspectorPanel` host | 2h | New `src/inspector/VSILInspectorPanel.tsx`. Wraps the 5 sub-modes with chip row + layout. Takes `source` as prop. | low |

| 2 | Replace `useInspectorSource` stub with real impl | 1h | Move the `parseVSIL → buildSceneGraph → extractInspectorSource` pipeline from `InspectorCard.tsx` into a real `useInspectorSource({ source })` hook in `useInspectorSource.ts`. | low |

| 3 | Add `EventTarget` bus | 1h | `src/inspector/eventBus.ts` — 20 lines, `const bus = new EventTarget()`. Export + add `dispatchVSILSourceChanged(bus, source)` helper. | low |

| 4 | Add `useVSILInspectorEvents` hook | 1h | `src/inspector/useVSILInspectorEvents.ts` — subscribes to bus, returns latest `InspectorSource`. | low |

| 5 | Add `capabilities.ts` manifest | 1h | Frozen manifest per §5. Export `VSIL_INSPECTOR_CAPABILITIES` + `InspectorCapabilities` type. | low |

| 6 | Rewrite `InspectorCard.tsx` as thin wrapper | 1h | The card in VibeScope becomes `<VSILInspectorPanel source={vsilSource} />` — no business logic. **VibeScope still works unchanged.** | low |

| 7 | Add `VSILPanelMount.tsx` for PM2 / Edge Panel | 2h | Standalone hostable panel root. Reads `data-vibescope-asset-id` attribute, fetches source from URL, mounts `<VSILInspectorPanel>`. 5-line PM2 contract: `mgr.addPanel({ id: 'vsil-inspector', title: 'VSIL Inspector', content: <VSILPanelMount />, icon: 'inspector' })`. | low |

| 8 | Tests + drift-check run | 2-3h | (a) Add `__tests__/VSILInspectorPanel.test.tsx` — 4 cases: renders with source, switches sub-modes, READ-ONLY invariant (no onClick), event subscription. (b) Run DC-VC-1..4 drift-checks. (c) Run inspector-copy-001 + inspector-module-scope-001. (d) 1 Playwright smoke: open VibeScope, click Inspector, see 5 sub-modes. | low |

**Total: 11-13 hours** (well under 2-3 days).

What does NOT change

What gets added (NOT changed)

What gets rewritten

---

9. The 5-line PM2 contract (post-refactor)

```ts

import { VSILPanelMount } from 'studio/modules/vibescope/src/inspector/VSILPanelMount';

import { VSIL_INSPECTOR_CAPABILITIES } from 'studio/modules/vibescope/src/inspector/capabilities';

// 1. Capability check

if (VSIL_INSPECTOR_CAPABILITIES.can.inspectSource) {

// 2. Mount in PM2

mgr.addPanel({

id: 'vsil-inspector',

title: 'VSIL Inspector',

content: <VSILPanelMount initialSourceUrl="/api/vsil/current" />,

icon: 'inspector',

on: { edge: 'right' },

});

}

```

**No iframe. No postMessage. No shadow DOM. Just a React component mounted in a panel.**

---

10. Cross-references

---

11. The 3 open questions for the operator

| # | Question | Default if no answer |

|---|----------|---------------------|

| Q1 | Should `InspectorCard.tsx` be deleted (VibeScope uses `<VSILInspectorPanel>` directly) or kept as a thin wrapper for back-compat? | thin wrapper (VibeScope keeps working unchanged) |

| Q2 | Should the `useInspectorSource` hook accept a `sourceUrl` (auto-fetch) prop, or require pre-loaded `source`? | both: `source` preferred, `sourceUrl` fallback |

| Q3 | Where does the `EventTarget` bus live — module-scoped (one shared instance) or context-scoped (per-workspace)? | module-scoped (1 line) for v1, context-scoped if we add multi-workspace later |

---

12. The 1 thing to remember

> **The Inspector proper is already 80% of the way there.** The 688 LoC in `src/inspector/` is clean (public API, frozen types, READ-ONLY invariant). The drift is in the 278 LoC `cards/InspectorCard.tsx` glue layer. The refactor moves that glue out, makes the panel host-agnostic, and adds 5 new files (~250 LoC) that the chrome layer can use. **No iframe. No pact edits. No extraction. Just panel-as-component.**

---

**Discovery-only. No code. No pact edits. No extraction. No implementation.**

— vibecoder-standalone-mavis (thread 9)