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
84 changes: 84 additions & 0 deletions packages/shared/__tests__/skill-md-sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Lock-in test: every node type the schema accepts must be documented in
* skills/openhop/SKILL.md, and SKILL.md must not advertise any type the
* schema rejects. Drift between them is a launch-blocking bug — agents
* read SKILL.md to learn the valid type list, write `type: foo`, and the
* server rejects the flow.
*
* This test runs from the workspace-root skills/openhop/SKILL.md, which
* is the canonical source. (The cli's prepack copies it into
* packages/cli/skills/ at publish time; we don't validate that copy here
* because it's regenerated.)
*/

import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { NodeTypeEnum } from '../src/schema.js'

const here = dirname(fileURLToPath(import.meta.url))
const SKILL_MD = resolve(here, '..', '..', '..', 'skills', 'openhop', 'SKILL.md')

const KNOWN_TYPES = new Set<string>(NodeTypeEnum.options)

/** Pull type names from SKILL.md's "Node Type Variants" table only.
* The first column is the type; subsequent rows include `Type` (header),
* the `---` separator, then one row per type. Stop at the next blank
* line after the table starts so unrelated tables (like patch ops) don't
* leak in. */
function readSkillTypeTable(): string[] {
const md = readFileSync(SKILL_MD, 'utf-8')
const sectionStart = md.indexOf('Node Type Variants')
expect(sectionStart, 'SKILL.md must have a "Node Type Variants" section').toBeGreaterThan(0)
const slice = md.slice(sectionStart)
const types: string[] = []
let inTable = false
for (const line of slice.split('\n')) {
if (line.startsWith('|')) {
inTable = true
const m = line.match(/^\|\s*([a-z][a-z0-9-]*)\s*\|/)
if (m && !/^-+$/.test(m[1]) && m[1] !== 'type') types.push(m[1])
} else if (inTable && line.trim() === '') {
break // table ended
}
}
return types
}

describe('SKILL.md / schema NodeTypeEnum sync', () => {
it('every type SKILL.md advertises is accepted by the schema', () => {
for (const t of readSkillTypeTable()) {
expect(KNOWN_TYPES.has(t), `SKILL.md mentions type "${t}" but schema rejects it`).toBe(true)
}
})

it('every schema type appears in the SKILL.md variants table', () => {
const advertised = new Set(readSkillTypeTable())
for (const t of NodeTypeEnum.options) {
expect(
advertised.has(t),
`Schema accepts type "${t}" but SKILL.md does not document it`
).toBe(true)
}
})

it('the type-bullet line in SKILL.md lists every schema type', () => {
const md = readFileSync(SKILL_MD, 'utf-8')
// The bullet that documents the closed enum, e.g.
// `type`: closed enum, exactly one of: `actor | endpoint | …`
// We look for the bullet that has both `\`type\`` and at least one
// pipe-separated list of bare names.
const m = md.match(/`type`:[\s\S]{0,200}?`([a-z][a-z0-9 |]+)`/)
expect(m, 'SKILL.md must have a `type`: bullet listing the enum').not.toBeNull()
const listed = new Set(
m![1]
.split('|')
.map((s) => s.trim())
.filter(Boolean)
)
for (const t of NodeTypeEnum.options) {
expect(listed.has(t), `SKILL.md type-bullet missing "${t}"`).toBe(true)
}
})
})
93 changes: 73 additions & 20 deletions skills/openhop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,49 @@ allowed-tools: Bash(openhop:*), Bash(npx tsx:*)

OpenHop renders animated data flow diagrams. You describe the flow in YAML, push it with the CLI, and the user sees animated data pixels traveling between components.

## Quickest valid flow (copy this, modify ids/labels)

This is the **smallest known-valid flow**. Start from this and edit — do not invent the schema.

```yaml
meta:
title: Three-tier app
flow:
nodes:
- id: browser
label: Browser
type: actor
- id: api
label: API
type: endpoint
- id: db
label: Postgres
type: database
steps:
- from: browser
to: api
data: request
- from: api
to: db
data: query
- from: db
to: api
data: rows
- from: api
to: browser
data: response
```

Push it with `openhop push <file>` (or `openhop push -` for stdin). On success you get a flow id and a URL.

**Validation rules to lock in before you write your own:**

- `type` must be one of the 12 enum values (see Schema Reference below). `transform`, `validation`, `redis`, `oauth`, etc. are **not** valid types.
- `data` is a `string` or an object — never a list. `data: "request"` ✓, `data: { label: "request", fields: [...] }` ✓, `data: [{ name: "request" }]` ✗
- `id` is alphanumeric + hyphens + underscores only.

If the validator rejects your flow, **read the error path** — it tells you exactly which field is wrong.

## Before Creating Flows

Verify the OpenHop API server is running:
Expand Down Expand Up @@ -144,16 +187,21 @@ openhop patch abc123 polish-patch.yaml
Prefix all commands with the repo path:

```bash
openhop serve # Start server
openhop serve # Start server + frontend
openhop push <file.yaml> # Push a flow → returns ID and URL
openhop serve # Start API + web UI (:8787 + :8788)
openhop validate <file.yaml> # Local schema check, no server needed
openhop validate - # Validate from stdin
openhop push <file.yaml> # Push a flow → returns id + URL
openhop push - # Push from stdin (pipe YAML)
openhop get <flow-id> # Fetch a flow by id (full JSON)
openhop list # List all flows
openhop patch <flow-id> <patch.yaml> # Apply patch operations
openhop patch <flow-id> - # Patch from stdin
openhop list # List all flows
openhop remove <flow-id> # Delete a flow
openhop help --json # Full machine-readable command tree
```

Every command supports `--json` for machine-readable output. Use it whenever you'll parse the result. Exit codes are semantic: `0` success, `2` usage, `3` validation, `4` not-found, `5` conflict, `6` network. **Always `validate` before `push`** when iterating — it skips the server round-trip.

Stdin is useful when generating YAML programmatically:

```bash
Expand All @@ -177,29 +225,34 @@ flow:
### Node

- `id` (required): alphanumeric + hyphens + underscores
- `label` (required): display name
- `type`: actor | endpoint | transform | validation | auth | database | external | cache | queue | service | custom
- `icon`: Iconify icon ID (e.g. "logos:postgresql") — overlays on top of the node's pixel art. Works on any `type`, not just `custom`. Browse: https://icon-sets.iconify.design/logos/
- `label` (required): display name — **freeform**, anything human-readable (`"Stripe Payment Gateway"`, `"Customer #1"`, `"Order Service v2"`)
- `type`: **closed enum, exactly one of**: `actor | endpoint | auth | database | external | cache | queue | service | docker | k8s | scheduler | custom`. Anything else fails validation. Default if omitted: `service`.
- `icon`: Iconify icon ID (e.g. `"logos:postgresql"`) — overlays on top of the node's pixel art. Works on any `type`, not just `custom`. Browse: https://icon-sets.iconify.design/logos/
- `color`: hex color
- `flow`: nested sub-flow { nodes, steps } — makes node expandable with +

> **Critical: types are categories, labels are names.** The 12 `type` values are how the renderer knows which sprite + color to draw (database = barrel, cache = lightning, etc.). The `label` is what the user reads on the node. **Never** put a variant name (like `redis`, `oauth`, `stripe`) into `type` — that's a label. Put it in `label`, and use the matching category in `type` (`cache`, `auth`, `external`).
>
> **When nothing fits**, use `type: custom` and set your own `icon` + `color`. Don't invent new type values — the schema is closed.

### Node Type Variants (pick the right type, then a concrete instance)

Each node type has common real-world variants. Use them to choose an accurate `label` and, where applicable, a matching Iconify icon. First entry is the canonical/most common variant for that type.

| Type | Common variants |
| ---------- | ------------------------------------------------------------------------------------------------------------ |
| actor | user, admin, customer, operator, agent, bot, service-account, system |
| endpoint | rest-api, graphql, grpc, webhook, websocket, sse, rpc |
| transform | parser, serializer, formatter, mapper, aggregator, filter, enricher |
| validation | schema-validator, input-validator, permission-check, rate-limiter, csrf, feature-flag |
| auth | oauth, jwt, session, api-key, saml, ldap, mfa |
| database | postgres, mysql, mongodb, sqlite, cassandra, dynamodb, cockroachdb, bigquery, snowflake, elasticsearch, disk |
| external | stripe, twilio, sendgrid, github, slack, openai, anthropic, firebase, s3, maps-api |
| cache | redis, memcached, ram, cdn, http-cache, local-cache |
| queue | kafka, rabbitmq, sqs, pubsub, nats, kinesis, celery |
| service | microservice, worker, scheduler, processor, orchestrator, gateway, proxy, loadbalancer |
| custom | (anything — also set `icon` and `color`) |
| Type | Common variants (use as `label`, NOT `type`) |
| --------- | ------------------------------------------------------------------------------------------------------------ |
| actor | user, admin, customer, operator, agent, bot, service-account, system |
| endpoint | rest-api, graphql, grpc, webhook, websocket, sse, rpc |
| auth | oauth, jwt, session, api-key, saml, ldap, mfa |
| database | postgres, mysql, mongodb, sqlite, cassandra, dynamodb, cockroachdb, bigquery, snowflake, elasticsearch, disk |
| external | stripe, twilio, sendgrid, github, slack, openai, anthropic, firebase, s3, maps-api |
| cache | redis, memcached, ram, cdn, http-cache, local-cache |
| queue | kafka, rabbitmq, sqs, pubsub, nats, kinesis, celery |
| service | microservice, worker, processor, orchestrator, gateway, proxy, loadbalancer |
| docker | container, sidecar, init-container, compose-service |
| k8s | pod, deployment, statefulset, daemonset, job, cronjob, service, ingress |
| scheduler | cron, airflow, temporal, celery-beat, sidekiq, bullmq |
| custom | (anything — also set `icon` and `color`) |

### Step

Expand Down
Loading