# Typed posts & relations — typing is just relating to a type

> host-on-sx. Driving idea: **classification is a relation to a type node, and
> types are posts.** Everything (related, tag, category, series, type) becomes a
> typed edge in `lib/relations` over `blog:<slug>` nodes. One primitive.

## Decisions

- **Types are posts.** No new node namespace — content-posts and type/tag posts
  are all `blog:<slug>`. A "tag" is a post; tagging documents itself.
- **`is-a` is the typing edge; `tagged` is membership.** Kept distinct so a tag
  page can list members without conflating "ocaml is a tag" with "hello is
  tagged ocaml".
- **Hierarchy is core, not deferred.** `is-a`/`subtype-of` transitive closure via
  `lib/relations` reachability is what makes typing-as-relation more than flat
  labels. All typing helpers are transitive from the first line, or subtypes
  silently break candidate/`is-a?` checks later.
- **Validation is gradual, not deferred.** A type-post *optionally* carries a
  schema slot; validation runs only where one exists. Tags declare none (stay
  folksonomy-free); `article` can declare "needs a heading". The hook lands with
  the type phase (reusing `host/blog-content-ok?`); only schema *expressiveness*
  grows over time. This closes the nominal/structural loop: the declared `is-a`
  edge is a claim, the validator checks the content honors it.
- **Scalars stay fields.** `status`/`title`/`sx_content` remain fields, not edges
  — listings filter on them constantly and `lib/relations` re-saturates Datalog
  per query. Links-to-shared-nodes → edges; per-post hot scalars → fields.

## The linchpin: a relation-kind registry

One data structure drives validation, the picker candidate sets, and rendering:

```
host/blog-rel-kinds =
  ({:kind "related" :label "Related posts" :symmetric true  :candidates "all"}
   {:kind "is-a"    :label "Types"         :symmetric false :candidates "types"
                    :inverse-label "Instances"}
   {:kind "tagged"  :label "Tags"          :symmetric false :candidates "tags"
                    :inverse-label "Tagged with this"})
```

`:symmetric` → write both directions on relate. `:candidates` → what the picker
offers (`all` = every post; `tags` = `is-a? blog:tag` transitively; `types` =
`is-a? blog:type`). `:label`/`:inverse-label` → headings.

## Phases

### Phase 1 — Kind generalization + registry  ← START HERE
Pure refactor; zero user-visible change (related keeps working).
- `host/blog-rel-kinds` registry + `host/blog--kind-spec`/`--kind-symmetric?`.
- `host/blog-relate!(a,b,kind)` / `unrelate!(a,b,kind)` — directed; symmetric kinds
  also write the reverse (today's "related" behavior = the symmetric case).
- `host/blog-out(slug,kind)` (children) / `host/blog-in(slug,kind)` (parents),
  existence-filtered. `host/blog-related(slug)` = `out(slug,"related")` (back-compat).
- Routes carry `kind` (form field, default `"related"`); validated against registry.
- `delete` cleanup drops edges across **all** kinds, both directions.

### Phase 2 — Type resolution via reachability (the spine)
- Seed root type-posts: `blog:type` ("Type") and `blog:tag is-a blog:type`,
  each documenting itself. Idempotent seed in `serve.sh`.
- `host/blog-types-of(slug)` = direct `is-a` targets ∪ `subtype-of`-reach of each
  (SX-side composition over `lib/relations` reach — no new Datalog rules).
- `host/blog-is-a?(slug, type)` — **transitive**.
- Type-posts carry an optional `:schema` slot (designed now, mostly empty).
- Validation hook: `host/blog-content-ok?` extended to also run any schema(s)
  implied by the post's declared types. No schema → no-op (gradual).

### Phase 3 — Tags as posts
- "is a tag" = `host/blog-is-a? slug "tag"` (transitive). Helpers
  `host/blog-tags(slug)` = `out(slug,"tagged")`, `host/blog-tagged-with(tag)` =
  `in(tag,"tagged")`.
- Edit page: a "This post is a tag" toggle = add/remove `is-a blog:tag` edge.

### Phase 4 — Render (data-driven from the registry)
- Post page iterates the registry → "Related posts" + "Tags" blocks, same code.
- Tag-post page: its own content (the tag's documentation) **plus** "Tagged with
  this" (incoming `tagged`). A tag page documents the tag AND lists its members.
- Optional `/tags` index = posts `is-a? blog:tag`.

### Phase 5 — Generalize the picker
- `host/blog--relate-candidates(slug, q, kind)` branches on the kind's
  `:candidates` (all / tags / types).
- `relate-options` endpoint takes `&kind=`; picker filter input carries
  `data-kind`; `relate-picker.js` forwards it.
- Edit page renders one picker section per kind from the registry.

### Phase 6 — Schema expressiveness (ongoing)
- Grow the type `:schema` language: start minimal (required block kinds / a
  predicate over content), richer later. Enforcement already wired in Phase 2;
  only the language grows. Not a blocker — a gradient.

## Notes
- Node model unchanged (`blog:<slug>`); only `kind` varies. The relate machinery,
  picker, and post-page block all generalize by lifting the hard-coded
  `kind: "related"` into a parameter.
- A type can *be* a post all the way up (`blog:tag is-a blog:type`); meta-circular
  but bounded by seeding a small root set.
