# Guide 2: HEART-Embedded photo editing software

This guide takes a photo editor from day 1 to an application that knows its own health, protects people's photos, and serves plain requests through one standard language.

The finished example is in `heart-core-rs/examples/photo-editor`. Its central flow runs in `conformance/wasm.mjs`: a plain request, then approval, then the app's own export function.

---

## Before you start: the two rules for a creative app

1. **People's photos never leave the computer through HEART.** Pixels, file paths and location data are `never-share`. If you add cloud AI, it receives summaries such as an exposure histogram, never the image.
2. **HEART asks before it changes someone's work.** Exporting, enhancing or deleting always shows a confirmation, even when the request came in plain words.

---

## Day 1: Choose how HEART lives in your app

An app does not need a network port. The Core runs **inside** the app.

| Your app is built with | Use | Status |
|---|---|---|
| JavaScript or TypeScript (web, Electron) | `heart-wasm`, in a Web Worker | Works today, tested in Node |
| Rust (Tauri, egui) | Link `heart-core` directly | Works today |
| C++, C#, Swift | Run `heartd` beside the app | Partial: `heartd` cannot yet call functions inside your app. You would need a local HTTP adapter, which is not written yet, or command-line versions of your features. |

The rest of this guide uses the WebAssembly route.

---

## Step 1: Give the app an identity (the HEART Card)

```json
{
  "system": { "id": "com.example.lumen.photo-editor", "name": "Lumen Photo Editor",
              "version": "0.1.0", "alias": "lumen" },
  "level": "full",
  "bindings": ["http-push", "in-process"],
  "pulse": { "intervalSec": 60 },
  "modules": ["custom", "core"],
  "shares": { "pulse": "public", "rhythm": "internal", "userData": "never-share" },
  "valve": {
    "llmCeiling": "internal",
    "classifications": [
      { "path": "rhythm:/attributes/filePath", "level": "never-share" },
      { "path": "rhythm:/attributes/gps", "level": "never-share" }
    ]
  },
  "ai": { "modes": ["byok"], "providers": ["any"], "offline": "rules-only" }
}
```

`level` must be `full`, because plain-language serving creates actions that need approval. Testing caught this: a Card set to `connected` could create such actions but never approve them. The standard now states the rule.

---

## Step 2: Decide what is private

| Item | Privacy |
|---|---|
| App healthy or not | `public` |
| Open documents, export queue, GPU memory, crash count | `internal` |
| Names of the user's projects | `private` |
| Pixels, file paths, EXIF location, faces | `never-share` |

Even a field you forget to classify is treated as `private`. HEART fails closed.

---

## Step 3: Define what the app can do

Each feature that plain language or other tools may use becomes a **command definition** with `protocol: "function"`, meaning "the app performs this itself".

```json
{
  "name": "com.example.lumen.export_image",
  "kind": "command",
  "title": "Export image",
  "description": "Export the open image as JPEG, PNG or WebP at a chosen width.",
  "inputSchema": { "type": "object", "required": ["format", "width"], "additionalProperties": false,
    "properties": { "format": { "type": "string", "enum": ["jpeg", "png", "webp"] },
                    "width":  { "type": "integer", "minimum": 16, "maximum": 20000 } } },
  "risk": "write",
  "privacy": "internal",
  "callers": ["human", "ai"],
  "native": { "protocol": "function", "target": "app",
              "request": { "call": "exportImage", "format": "{{arguments.format}}", "width": "{{arguments.width}}" } }
}
```

The example defines **export image**, **auto enhance**, **clear preview cache**, and a property **GPU memory used**.

Text inputs must be limited by `enum` or `pattern`. HEART refuses a definition with a free-text input, because free text could inject a command.

---

## Step 4: Connect the Core to your app

```js
// heart-worker.js
const { instance } = await WebAssembly.instantiate(wasmBytes, {});
const w = instance.exports;

function send(fn, value, ...rest) {
  const bytes = new TextEncoder().encode(JSON.stringify(value));
  const ptr = w.heart_alloc(bytes.length);
  new Uint8Array(w.memory.buffer, ptr, bytes.length).set(bytes);
  return fn(ptr, bytes.length, ...rest);
}
const result = () => JSON.parse(new TextDecoder().decode(
  new Uint8Array(w.memory.buffer, w.heart_result_ptr(), w.heart_result_len())));

// Start, with a fresh random seed each time.
send(w.heart_start, { card, custom, rules, allowlist: ["com.example.lumen.clear_cache"],
                      instance: "desktop-1", seed: Array.from(crypto.getRandomValues(new Uint8Array(32))) }, Date.now());
setInterval(() => w.heart_tick(Date.now()), 1000);

// Every request goes through one function.
async function api(request) {
  send(w.heart_api, request, Date.now());
  let r = result();
  while (r.effect) {                         // HEART needs the app to do something
    const outcome = await perform(r.effect); // your code
    send(w.heart_resume, { token: r.token, ...outcome }, Date.now());
    r = result();
  }
  return r;                                  // { status, body }
}

// Carry out effects: your app's own functions, or a model call.
async function perform(effect) {
  if (effect.kind === "native" && effect.native.protocol === "function") {
    const { call, ...args } = effect.native.request;
    try { return { ok: await app[call](args) }; } catch (e) { return { error: String(e) }; }
  }
  return { error: "not supported" };
}
```

HEART itself never touches files or the network. It asks your app through effects, so it can never do anything your code does not allow.

---

## Step 5: Report what happens (the host feed)

```js
send(w.heart_report, { event: "autosave.failed", severity: "error", summary: "Disk full" }, Date.now());
send(w.heart_report, { vital: "gpuMemoryShare", value: 0.93 }, Date.now());
send(w.heart_report, { event: "plugin.crashed", component: "film-grain-plugin" }, Date.now());
```

Never put a file path or image data in a report. Use counts, sizes and the names of your own components.

---

## Step 6: Reflexes (Tending rules)

| Rule | When | Then |
|---|---|---|
| `gpu-memory-high` | GPU memory above 90% | Alert, then **clear the preview cache automatically** (allowlisted, reversible) |
| `plugin-crashes` | A plugin crashes 3 times in 10 minutes | Alert: "A plugin is unstable" |
| `autosave-failed` | Any autosave failure | Error alert, so the user does not lose work |

Testing confirmed that reporting `gpuMemoryShare: 0.95` raises the alert straight away. Only `clear_cache` may run without asking, because it is listed in `allowlist` and is reversible.

---

## Step 7: Plain-language requests in the app

Wire your command bar to `POST /heart/v1/serve`. With no AI model, these are the tested results:

| Request | Result |
|---|---|
| "Export the image as png 1080 wide" | `export_image`, png, 1080, **waiting for approval** |
| "Auto enhance the photo strength 40" | `auto_enhance`, 40, waiting for approval |
| "How much GPU memory is used?" | Answered at once |
| "Make my photo look better" | Not understood without a model. HEART does not guess |
| "Delete all my photos" | Not understood. There is no such command, so it cannot happen |

When the result is `pending-approval`, show a dialog with the exact values. When the person confirms, call `POST /heart/v1/actions/{id}/decision` with `approve`. HEART then asks your app, through an effect, to run `exportImage`.

---

## Step 8: Bring your own key (cloud AI)

With `ai.modes = ["byok"]`, each person enters their own provider key in your settings. The key stays on their computer, and HEART never sees it.

When HEART needs a model, it hands your app a `consult` effect holding a provider-neutral request that has already passed the Valve. Your app sends it to the provider with the user's key, and returns the reply. Set a daily budget in your settings screen.

HEART checks every answer: a model can only choose from commands that exist, and every value it claims the person said must actually appear in what they said.

---

## Step 9: Let your support team see app health (optional)

If you run a HEART Gateway, the app can push its Card and Pulse there. Only the levels you choose leave the computer, and never photos. Your team then sees `heart://support-hub/systems/lumen/pulse` for each installation that opted in.

Ask the user's permission first, and record it.

---

## Step 10: Test before shipping

| Test | How |
|---|---|
| Definitions load | `heartd check examples/photo-editor/heartd.toml` |
| The approval flow works | `node conformance/wasm.mjs` runs plain request, approval and export |
| Nothing private leaks | Report events with fake paths, read them back at `public` clearance, and confirm they are gone |
| Approval is required | No change happens without a decision |
| Plain language | Collect 50 real requests from beta users and score them |

---

## What is not ready yet

- Calling app functions from a separate `heartd` process. Today it works only with the Core inside the app.
- A TypeScript package and a C interface. The example calls the WASM exports directly.
- Use beyond testing in Node. It has not run inside a real photo editor yet.

---

## Timeline

| When | What |
|---|---|
| Day 1 | Steps 1 and 2: identity and the privacy table |
| Week 1 | Steps 3 to 5: definitions, connecting the Core, the host feed |
| Week 2 | Steps 6 and 7: reflexes, command bar and approval dialog |
| Week 3 | Step 10 with beta users |
| Later | Steps 8 and 9 |
