# Publishing + iterating

You have an instrument, a sample, a knowledge bundle, a full project. You want to share it. This section covers the publish flow, version bumps, forks, the My Items view, and the safety rails for deletion + account housekeeping.

## The canonical publish path

The UI path: **Hub tab → My Items → Upload**. Pick a kind, point at the source file, fill in slug + title + license + visibility + description + tags, hit Upload. ONE door, deliberately — the IDE used to have "Publish" buttons in module footers (Builder, editors), they got pulled because three doors invited inconsistency. One form, one path.

The MCP twin:

```ts
hub.publish {
  file_path: "asset:assets/my-instrument.builder",
  kind: "builder",
  slug: "my-saturator-lead",
  title: "Saturator Lead",
  license: "CC0-1.0",
  visibility: "public" | "unlisted" | "private" | "friends" | "team",
  description?: "...",
  tags?: ["lead", "saturator", "synth"],
  changelog?: "v1 — initial release",
}
```

Reads the file, validates the content for the requested kind, creates the item metadata row, uploads the content as v1. Returns the new ItemDto.

> 🟦 **WHY THE FILE-PATH PATTERN?** Hub items are content-driven — the `.builder` document IS the DSP, the `.md` IS the knowledge pack. Publishing references a file on disk rather than in-memory state so what's published exactly matches what's on disk. Round-trip-clean: install your own item, you get bytes-identical content back.

## A publish chain end-to-end

A Builder → publish workflow, all MCP:

```yaml
actions:
  - action_id: documents.add
    input: { type: builder, title: "My instrument" }
    save_as: b
    label: "▶ 1. Spawn a Builder"
  - action_id: builder.graph.node.add
    input: { module_id: "{{b.module_id}}", kind: osc }
    label: "▶ 2. Add an oscillator"
  - action_id: builder.graph.node.add
    input: { module_id: "{{b.module_id}}", kind: output }
    label: "▶ 3. Add an output"
  - action_id: documents.save
    input: { module_id: "{{b.module_id}}" }
    save_as: saved
    label: "▶ 4. Save the DSP (returns the .builder ref)"
  - action_id: hub.publish
    input:
      file_path: "{{saved.filePath}}"
      kind: builder
      slug: my-osc-test
      title: "My osc test"
      license: CC0-1.0
      visibility: unlisted
      description: "Tiny osc → output test DSP"
      tags: [test, osc]
    label: "▶ 5. Publish as v1, unlisted"
```

> 🟦 **HANDS-FREE PATH**: chains the whole flow. Step 4 saves the DSP and returns its on-disk path; step 5 hands that path to `hub.publish`. The crucial trick is `documents.save` for `builder` (it links the file to the module so the project re-opens it) — don't use `builder.export` here, that writes a file but doesn't link it.

## Slug + title + license + visibility — the required fields

| Field | Rules |
|---|---|
| **slug** | `[a-z0-9-]+`, max 64 chars, **unique per owner across kinds**. Immutable after publish — you'd have to fork into a new slug to change. |
| **title** | Display name. Mutable via `hub.item.update`. |
| **license** | SPDX identifier. Mutable via `hub.item.update` (with care — retroactively changing your license is awkward for installers). |
| **visibility** | `public` / `unlisted` / `private` / `friends` / `team`. Mutable. Start `unlisted` if you want a soft launch. |

Optional but recommended: **description** (what it does, who it's for), **tags** (a handful, surfacing in search), **changelog** (what's in this version).

## Uploading a new version

You shipped v1, improved it, want to push v2:

```
hub.version.upload {
  item_id: "<id from hub.items.my>",
  file_path: "asset:assets/my-DSP-v2.builder",
  kind: "builder",
  changelog: "v2 — replaced moog_vcf with a steeper SVF"
}
```

`kind` is optional — omit it and the action does a server roundtrip to resolve the kind from the item record. Pass it to save the roundtrip.

Server assigns the version number (`v(n+1)`). Users on "latest" automatically get v2 on next install. They can browse to v1 if they want.

**The user manual you're reading uses this loop — every chapter rebuild is one `hub.version.upload` per affected pack.**

## Updating metadata without uploading

No new content, just tweaks?

```
hub.item.update {
  item_id: "<id from hub.items.my>",
  title: "Saturator Lead (refined)",
  description: "Updated description with the new param list",
  tags: [lead, saturator, synth, fm],
  visibility: public
}
```

Slug and kind are immutable; everything else is mutable. Sends only the fields you pass — omitting `tags` leaves them alone (it doesn't blank them).

## Forking somebody else's work

Found a great DSP you want to evolve as your own variant?

```
hub.fork { item_id: "<source id from hub.search>" }   → fresh ItemDto in YOUR namespace
```

Returns a fresh ItemDto in YOUR namespace. v1 of the fork = the source's latest version, server-side copied. You own the fork; the parent stays untouched. Same-kind only — you can't fork a `builder` into a `dsp`.

Forks are how community remixes happen — somebody publishes a great FM bass DSP, you fork it + add an envelope + upload as YOUR fork v2. Parent + fork co-exist.

## My Items — your dashboard

The Hub tab's **My Items** view shows every item you've published — public, unlisted, private — as one list. Filter by kind. Edit metadata in-place. Upload new versions. Delete items.

```yaml
actions:
  - action_id: hub.items.my
    input: { kind: book }
    save_as: my_kbs
    label: "▶ List my books"
```

## Deleting items

Destructive operation — confirm before firing:

```
hub.item.delete { item_id: "<id from hub.items.my>" }   → permanent; bytes go too
```

Gone is gone. Versions, artifacts, install URLs — all invalidated. People who installed older versions keep their local copies; new install attempts after delete fail.

## Account housekeeping

A single safety-rail action covers the heaviest user op:

- `hub.account.delete` — **PERMANENT, IRREVERSIBLE**: deletes your account, all published items, your bearer token. Signs you out and returns you to the signed-out shell.

The UI path has a type-to-confirm modal; the MCP path doesn't. The AI assistant is the safety net — it refuses to call `hub.account.delete` unless your most recent message is unambiguous (e.g. "delete my account"). Casual phrasing like "clean up my Hub" gets a confirm-first response, not an immediate call.

## How the FaustWave manual is itself published

Meta moment — the manual you're reading right now lives on the Hub as 10 book items (one per domain). Loop:

1. Author / edit a chapter as a markdown file in the markdown-editor module.
2. Save to `docs/books/<slug>/<NN>-chapter.md`.
3. Run `node docs/books/build-books.mjs` — builds one `_build/<slug>.md` per pack.
4. `hub.version.upload { item_id: <pack id>, kind: "book", file_path: <build artifact>, changelog }`.
5. `hub.install { item_id }` to re-index locally so the Reader picks up the new version.

Author + reader are the same FaustWave instance. The manual evolves at the speed of the IDE.

## Common moves

- **"Publish a draft I'll share with one friend"** — visibility `unlisted`, send them the item id. They install with `hub.install`.
- **"Bulk-update tags on every book I own"** — `hub.items.my { kind: "book" }` then iterate `hub.item.update` per id with new tags.
- **"Clone an item but with my own changes"** — `hub.fork`, then `hub.version.upload` to push your improved version on top of the source's v1.
- **"Roll back a broken version"** — no rollback action; instead upload the previous content as v(n+1) with a "reverting to vX content" changelog.

## Where to go from here

- **FaustWave — Builder** for the DSP side of `hub.publish kind: "builder"`.
- **FaustWave — Recorder** § 2 for the round-trip into sample-packs.
- **FaustWave — AI Assistant** for asking the assistant to publish on your behalf.
