# Constellation sky links

A **sky** is a map of skills for one activity: each skill is a **star**, and lines between stars say what to learn first. [Constellation](https://apps.apple.com/app/id6775833149) is an iPhone app for tracking practice this way, and a sky can also live entirely inside a link, at `https://sky.alexhumphreys.me/s/#…`. Everything after the `#` is the sky. The page that opens it runs in the browser and never sends the sky to a server.

This page explains the format, for people and for AI assistants. If someone asked you to make them a sky, the next section is all you need.

## Making a sky for someone

Write JSON in this shape:

```json
{
  "version": 2,
  "title": "Bouldering basics",
  "practices": [{ "name": "Bouldering" }],
  "stars": [
    { "name": "Straight arms" },
    { "name": "Quiet feet" },
    { "name": "Flagging", "prereqs": ["Straight arms"] },
    { "name": "Drop knee", "prereqs": ["Quiet feet"] },
    { "name": "Heel hook", "prereqs": ["Flagging"], "softPrereqs": ["Drop knee"],
      "notes": ["Pull with the hamstring, not the arms."] },
    { "name": "Dyno", "prereqs": ["Heel hook"] }
  ]
}
```

Then give it to them in one of two ways:

- **As a link:** `https://sky.alexhumphreys.me/s/#` followed by the JSON, percent-encoded (what JavaScript's `encodeURIComponent` produces). Tapping it opens the sky, ready to edit.
- **As JSON to paste:** on the sky page, the menu has **Paste a sky**. It accepts your whole answer and finds the JSON inside it, code fences and all.

Guidelines that make a sky useful:

- **Name stars after real, specific skills** in the activity's own words ("Heel hook", not "Intermediate technique 3").
- **`prereqs` means "learn this first".** A star stays locked until all its prereqs are landed. Use `softPrereqs` for "helps with, but not required". Don't make loops.
- **Start where they are.** If the person says what they can already do ("I know my open chords", "I can already stand up on a surfboard"), include those skills as stars with `"landed": true`, so the sky opens with their next steps lit.
- **Start small: 6 to 30 stars.** Long links break in some messaging apps, and a short sky is easier to start on.
- **Leave positions out.** The page lays the sky out on first open.
- **Don't invent links.** Only add a `links` entry for a URL you know exists; otherwise leave links out.
- **Say it's a draft.** Progressions for an activity you know second-hand may be plausible but wrong; the person will fix the order as they go.

Every line is drawn on the sky, so a sky with fewer, truer lines reads better. When choosing prereqs:

- **List only direct prereqs.** If "Harness basics" needs "Control power", and "Control power" needs "Neutral stance", don't also list "Neutral stance" for "Harness basics". It's already implied, and the extra line crosses the whole sky.
- **Test every hard prereq: could someone learn this star without ever having done that one?** If they could, it isn't a prereq. Use `softPrereqs` when it genuinely helps, and leave it out when the two are merely related, often practised together, or usually taught in that order.
- **Most stars need one prereq, a few need two, and three or more is rare.** When every new star depends on several earlier ones, the sky turns into a tangle instead of a map.
- **Separate tracks can stay separate.** Unconnected groups (say, footwork drills and finger strength) are drawn side by side. Don't tie them together with a catch-all star like "Basics" or "Warm-up" unless it really has to come first.
- **Watch for pairs that share both prereqs.** If two stars each need the same two earlier stars, check whether both lines are true. Four lines between two pairs always cross.
- **Keep names short:** one to four words, under about 25 characters. Names sit beside their stars, and long ones crowd each other out.

Names in `prereqs`, `softPrereqs`, `from` and `to` can be star names, as above, or ids. Anything the page can't use is dropped and listed, never an error.

## Fields

Top level:

- `version`: `2`.
- `title`: optional. The sky's heading, in your own words.
- `practices`: the activities in the sky, each `{ "name": "…" }`, optionally with a `tint` colour as `"#rrggbb"`.
- `stars`: the skills.

Each star:

- `name`: required.
- `practice`: the practice's name or id. Defaults to the first practice.
- `prereqs`, `softPrereqs`: stars this one needs, by name or id.
- `landed`: `true` if the person can already do it. Omit otherwise.
- `notes`: a list of plain-text notes. `[[Star name]]` inside a note links to that star.
- `links`: a list of `{ "url": "https://…", "title": "…" }`. Only `https` links are kept.
- `kind`: `"transition"` for a named move that bridges two stars, with `from` and `to`.
- `id`: optional. Short, `[A-Za-z0-9_-]`. The page adds ids when they're missing.
- `x`, `y`: optional position in a 2400 × 1600 sky, origin top left. Both or neither.

Unknown fields are kept and ignored, so links made by newer versions still open.

## Reading a link you were given

Take everything after the `#`.

- **If it starts with `{` or `%7B`**, it's JSON: percent-decode it and parse it.
- **Otherwise it's compressed.** Base64url-decode it to bytes:
  - byte 0: container version, `2`;
  - byte 1: flags; if bit 0 is set, the body is raw DEFLATE (no zlib header);
  - bytes 2 to 5: CRC-32 of the uncompressed JSON, big-endian;
  - the rest: the body. Inflate it if the flag is set; the result is the JSON above.

In Python:

```python
import base64, json, zlib
frag = link.split("#", 1)[1]
raw = base64.urlsafe_b64decode(frag + "=" * (-len(frag) % 4))
body = zlib.decompress(raw[6:], -15) if raw[1] & 1 else raw[6:]
sky = json.loads(body)
```

This link holds a two-star sky; decode it to check your reader:

`https://sky.alexhumphreys.me/s/#AgHoVDm3bZHBTsMwDIZfxTLXdCpDYmJXLkhw23HqITReG5YmJXG3VdMkXoPX40lwBmObxCWKrN-_v9_eo_W1GwwlnC_R6TEMjAp9YKlUCkO0jfU4x1WIneai1w0VSXe9I5H1Udds62PzHq0RXZtyu-5I_k_am8T5kRpbz1K7ma3MvS7xIOaRNji_VSiaeGGx1c6dTR5bSlxwKHIZ2uDMH58QL-yOR0hUB5mioM3DILVhcIZisbWGWwWvJPQEHEfrG9CwtvW6GPpJDniKcELfCdG0LBWOOH8oy4P6hco9Z6jnH4dLkpfBN6TABP_18cnwNnT9BBYiB2c9gU6wXOYIVfXv3D5SpPej0zF_dY0yuyvzxtiyu9os5N3li20oJhvkUtPDNw`

## Privacy

The sky lives in the link, not on a server: the part after `#` is never sent when a page loads. The page counts visits with [GoatCounter](https://www.goatcounter.com): it sends the page's path (`/` or `/s/`), the site you came from, and a `?from=` tag if the link has one, and nothing else. No cookies, and never the sky. Anyone with a link can read the whole sky, notes included, so share it the way you'd share the text inside it.
