# Kinds and validation

> Markdown kind definitions, dynamic Zod frontmatter schemas, status machines, indexedFields, slug rules, and immutable write semantics (append, status flip, supersede).

- Repository: superdesigndev/loopany
- GitHub: https://github.com/superdesigndev/loopany
- Human docs: https://grok-wiki.com/public/docs/superdesigndev-loopany-97bd9ab97ae8
- Complete Markdown: https://grok-wiki.com/public/docs/superdesigndev-loopany-97bd9ab97ae8/llms-full.txt

## Source Files

- `src/core/kind-registry.ts`
- `skills/loopany-core/kinds/task.md`
- `skills/loopany-core/kinds/signal.md`
- `skills/loopany-core/kinds/note.md`
- `src/core/slug.ts`
- `src/commands/artifact-status.ts`
- `test/kind-registry.test.ts`

---

---
title: "Kinds and validation"
description: "Markdown kind definitions, dynamic Zod frontmatter schemas, status machines, indexedFields, slug rules, and immutable write semantics (append, status flip, supersede)."
---

Loopany treats **kind** as an open registry: each kind is a Markdown file under `$LOOPANY_HOME/kinds/` (plus optional domain packs). At bootstrap, `KindRegistry` parses those files into Zod frontmatter validators, optional status machines, storage layout (`dirName`, `slugLayout`), and `indexedFields` for queries. `ArtifactStore` applies that metadata on every create, append, field update, and status transition.

## Kind definition format

A kind file has two layers of frontmatter:

| Layer | Location | Purpose |
| --- | --- | --- |
| Top YAML | Between the first `---` pair | `kind`, optional `dirName`, `slugLayout`, `indexedFields` |
| Field spec | `## Frontmatter` → fenced `yaml` block | Per-field types, `required`, `default`, enum `values` |
| Status machine | `## Status machine` → fenced `yaml` (optional) | `initial` + `transitions` map |
| Agent guidance | `## Required sections`, `## UI`, playbook prose | Conventions for agents; not parsed by the runtime |

`parseKindDefinition` in `src/core/kind-registry.ts` splits the body on `##` headings, extracts YAML from fenced blocks, and builds a `KindDefinition`.

**Defaults when omitted:**

| Field | Default |
| --- | --- |
| `dirName` | `{kind}s` (e.g. `task` → `tasks`) |
| `slugLayout` | `flat` |
| `indexedFields` | `[]` |

**`slugLayout` values:**

| Value | On-disk path |
| --- | --- |
| `flat` | `artifacts/<dirName>/<id>.md` |
| `year` | `artifacts/<dirName>/<YYYY>/<id>.md` (year = first four characters of `id`; used by `journal`) |

Example top-level kind header (`skills/loopany-core/kinds/note.md`):

```yaml
---
kind: note
dirName: notes
indexedFields: [tags]
---
```

Bundled kinds ship from `skills/loopany-core/kinds/`; `loopany init` copies any missing `*.md` into `~/loopany/kinds/` without overwriting existing files.

## Loading and extending the registry

```text
bootstrap (src/core/engine.ts)
  ├─ KindRegistry.load($LOOPANY_HOME/kinds)
  └─ merge packDirs: domains/<enabled>/kinds/*.md
       └─ duplicate kind or dirName → LoadIssue (first wins)
```

<ParamField body="packDirs" type="string[]">
  One directory per enabled domain from `config.yaml` `enabled_domains`. Missing domain `kinds/` dirs are skipped.
</ParamField>

`loopany kind list` prints JSON: `kind`, `dirName`, `slugLayout`, `indexedFields`, `hasStatusMachine`.

<Warning>
Broken kind files do not crash bootstrap. They appear in `registry.issues` and fail the `kinds` check in `loopany doctor`.
</Warning>

## Dynamic Zod frontmatter schemas

`buildZodSchema` maps each `## Frontmatter` field spec to a Zod type:

| `type` in kind file | Zod | Notes |
| --- | --- | --- |
| `string` | `z.string()` | |
| `enum` | `z.enum([...])` | Requires non-empty `values` |
| `date` | `z.string()` | ISO date string; no deep date parsing |
| `bool` | `z.boolean()` | |
| `string[]` | `z.array(z.string())` | CLI `set` accepts JSON array or comma-separated |
| `number` | `z.number()` | |

Fields with `default` get `.default(...)`; fields without `required: true` are `.optional()`. The object schema uses `.passthrough()`, so extra frontmatter keys (for example `createdAt`, `updatedAt`, `_backfilled`) are allowed.

**Built-in fields** accepted on every kind without appearing in the kind spec: `domain`, `createdAt`, `updatedAt`, `_backfilled`.

Validation runs on:

- `ArtifactStore.create` — after auto-filling `status` from the machine `initial`
- `ArtifactStore.setField` — after coercion
- `loopany doctor` — `safeParse` over every artifact in the index

<RequestExample>

```bash
loopany artifact create --kind task --title "Fix cache" --status todo
```

</RequestExample>

Missing `title` on a required field or an invalid enum value throws a Zod error at create time.

## Status machines

Kinds with a `## Status machine` block get runtime enforcement in `ArtifactStore.setStatus`. The CLI routes status changes through `artifact status`, not `artifact set`.

```mermaid
stateDiagram-v2
  [*] --> todo: create (initial)
  todo --> running
  todo --> done
  todo --> cancelled
  running --> in_review
  running --> done
  running --> failed
  running --> cancelled
  in_review --> done
  in_review --> failed
  in_review --> cancelled
```

(Transitions match bundled `skills/loopany-core/kinds/task.md`.)

| Behavior | Implementation |
| --- | --- |
| Initial status on create | Set to `statusMachine.initial` when `status` omitted |
| Legal transition | `newStatus` must be in `transitions[current]` |
| Illegal transition | Error: `Illegal transition: <current> → <newStatus>` |
| Kinds without a machine | `setStatus` throws; use `setField` for frontmatter-only kinds like `note` |
| `--reason` on CLI | Accepted by `artifact status` but **not** written to the artifact body |

**Signal-specific rule:** transitioning to `addressed` requires `--addressed-by <id>`. The CLI appends a hard `addresses` edge from that artifact to the signal (`src/commands/artifact-status.ts`).

**Terminal detection:** `loopany followups` hides artifacts whose current `status` has no outgoing transitions in the kind machine (unless `--include-done true`).

Kinds without status machines include `note`, `person`, and `journal`.

## Required body sections (agent-enforced)

Many bundled kinds declare `## Required sections` (for example `task`: `## Outcome` before `done` or `failed`). **The TypeScript runtime does not parse or enforce these rules** — `setStatus` does not inspect the body. Compliance is expected from agents following kind playbooks and capture/reflect skills. `loopany doctor` validates frontmatter only, not body shape.

Recommended terminal flow for `task`:

```bash
loopany artifact append <id> --section Outcome --content "..."
loopany artifact status <id> done --reason "one-line summary"
```

## Slug rules (v0.2)

The artifact **id is the slug** — globally unique across all kinds, no kind prefix. Files live at `artifacts/<dirName>/[<YYYY>/]<id>.md`.

`validateSlug` / `requireValidSlug` (`src/core/slug.ts`):

| Rule | Constraint |
| --- | --- |
| Length | 1–60 Unicode codepoints |
| Charset | Letters (`\p{L}`), marks (`\p{M}`), digits (`\p{N}`), `-`, `_` |
| Edges | No leading/trailing `-` or `_` |
| Runs | No `--` or `__` |
| Normalization | NFC before store/compare |

**ID allocation order** (`ArtifactStore.allocateId`):

1. Explicit `--slug` (validated, must not exist globally)
2. `slugifyTitle(title)` — lowercased, punctuation collapsed to `-`, max 40 codepoints; collisions get `-2`, `-3`, …
3. `generateFallbackSlug()` — `YYYYMMDD-HHMMSS-<3hex>` UTC when title slugify fails or is absent

<Info>
Cross-kind uniqueness is enforced by scanning every registered kind directory for `<id>.md` at create time.
</Info>

`note` and `person` playbooks expect human-chosen slugs (`project-phoenix`, `alice-chen`) for stable `[[wiki-links]]`.

## indexedFields

Declared in the kind file top frontmatter (for example `task`: `[status, priority, checkAt]`). At index build time, each listed field value is indexed per kind; array values index each element separately (`src/core/index.ts`).

Used by:

- `loopany artifact list --kind <K> --where <field>=<value>` when `<field>` is in that kind’s `indexedFields`
- In-memory `ArtifactIndex.byField(kind, field, value)`

Global indexes (not per-kind declaration): `status`, `domain`, `checkAt` (followups), kind name.

<Note>
`checkAt` is indexed for tasks but followups scans all artifacts with a `checkAt` frontmatter value regardless of kind.
</Note>

## Immutable write semantics

Loopany does not expose “edit artifact body in place” or “replace frontmatter wholesale.” Mutations are narrow operations:

```text
                    ┌─────────────────────────────────────┐
                    │  artifacts/<dirName>/[<Y>/]<id>.md  │
                    └─────────────────────────────────────┘
         create ──►│ frontmatter + body (initial)         │
         append ──►│ body: new H2 or append under H2      │  appendSection
         setField ─►│ frontmatter: one non-status field    │  re-validate Zod
         setStatus ►│ frontmatter: status only             │  machine check
         supersede ►│ NEW artifact + supersedes edge       │  (pattern, not store API)
                    │ OLD artifact: status flip only       │
```

| Operation | API | What changes |
| --- | --- | --- |
| Append body | `artifact append <id> --section <S> --content …` | Adds or extends `## <S>`; preserves other sections |
| Flip status | `artifact status <id> <status>` | `status` + `updatedAt`; body unchanged |
| Set field | `artifact set <id> --<field> <value>` | Single frontmatter field; **blocks `status`** |
| Supersede belief/version | Create new artifact + `refs add --relation supersedes` + `status` on old | Old file kept; graph records replacement (`learning`, `mission`, `brief` playbooks) |

`--reason` on status is for CLI/audit context only; it does not append a `## Status` section (see `test/artifact-store.test.ts`).

**Journal exception:** `journal` is auto-written by the store on other creates; `loopany artifact create --kind journal` is rejected at the CLI. Auto-append only touches `## Activity` (or `## Backfilled` for `_backfilled` artifacts).

**Entity kinds:** `person` documents append-only body timeline; frontmatter `name` / `aliases` may change via `setField`.

## Core bundled kinds (reference)

| Kind | `dirName` | Status machine | Notable `indexedFields` |
| --- | --- | --- | --- |
| `task` | `tasks` | todo → … → terminal | `status`, `priority`, `checkAt` |
| `signal` | `signals` | open / addressed / dismissed | `status` |
| `note` | `notes` | none | `tags` |
| `learning` | `learnings` | active / superseded / archived | `domain`, `status`, `checkAt` |
| `skill-proposal` | `skill-proposals` | pending → accepted/rejected | `status`, `targetSkill`, `domain` |
| `mission` | `missions` | yes | (see `kinds/mission.md`) |
| `brief` | `briefs` | yes | (see `kinds/brief.md`) |
| `person` | `people` | none | `aliases` |
| `journal` | `journal` | none; `slugLayout: year` | `date` |

## When to add a kind vs use `note`

Bundled `note` documents a four-question test: add a new kind only if you need a state machine, canonical identity/dedup, structured indexed queries, or a fixed body shape downstream code depends on. Otherwise use `note` or a domain scope (`skills/loopany-core/kinds/note.md`).

Domain-specific kinds belong under `domains/<name>/kinds/` and load after workspace kinds; conflicting `kind` or `dirName` values register as load issues.

## Verification

<Steps>
<Step title="List registered kinds">

```bash
loopany kind list
```

Expect bundled kinds with `hasStatusMachine` where applicable.

</Step>
<Step title="Run doctor">

```bash
loopany doctor
```

`kinds` and `artifacts` checks should pass on a healthy workspace.

</Step>
<Step title="Exercise validation">

```bash
loopany artifact create --kind task --title "Demo" --status bogus
```

Expect Zod/enum failure. Then create with `todo`, append `Outcome`, and `artifact status <id> running`.

</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Artifacts and workspace" href="/artifacts-and-workspace">
  v0.2 paths, `references.jsonl`, and slug-as-id storage layout.
</Card>
<Card title="Artifact commands" href="/artifact-commands">
  create, append, status, set flags and per-kind CLI fields.
</Card>
<Card title="Domains" href="/domains">
  Domain-scoped kind packs and `enabled_domains`.
</Card>
<Card title="Self-improvement loop" href="/self-improvement-loop">
  `## Outcome`, learnings, skill-proposals, and supersede flow.
</Card>
<Card title="Doctor and troubleshooting" href="/doctor-and-troubleshooting">
  Kind load failures, Zod errors, and recovery.
</Card>
</CardGroup>
