# Graph, search, and scheduling

> refs add/query, trace BFS, followups --due, hybrid search/reindex (--no-embed), factory UI (--port/--no-open), and audit.jsonl side effects.

- 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/commands/refs.ts`
- `src/commands/trace.ts`
- `src/commands/followups.ts`
- `src/commands/search.ts`
- `src/commands/reindex.ts`
- `src/core/search-store.ts`
- `src/commands/factory.ts`
- `src/ui/server.ts`

---

---
title: "Graph, search, and scheduling"
description: "refs add/query, trace BFS, followups --due, hybrid search/reindex (--no-embed), factory UI (--port/--no-open), and audit.jsonl side effects."
---

`loopany refs`, `trace`, `followups`, `search`, `reindex`, and `factory` operate on three derived views of `$LOOPANY_HOME`: append-only `references.jsonl` (explicit edges), an in-memory `ArtifactIndex` rebuilt each invocation (explicit + implicit edges, `checkAt` scheduling), and optional `search.db` (FTS5 + embeddings). Every successful or failed CLI run appends one row to `audit.jsonl`.

```mermaid
flowchart LR
  subgraph storage["$LOOPANY_HOME"]
    A["artifacts/*.md"]
    R["references.jsonl"]
    S["search.db"]
    AU["audit.jsonl"]
  end
  subgraph runtime["Per CLI invocation"]
    IDX["ArtifactIndex.build"]
    REFS["refs / trace"]
    FU["followups"]
    SR["SearchStore"]
  end
  A --> IDX
  R --> IDX
  IDX --> REFS
  IDX --> FU
  A --> SR
  SR --> S
  CLI["src/cli.ts"] --> AU
  REFS --> CLI
  FU --> CLI
  SR --> CLI
```

## Command overview

| Command | Mutates workspace | Primary output |
|---------|-------------------|----------------|
| `refs add` | Appends to `references.jsonl` | Single `Edge` JSON |
| `refs <id>` | Read-only | `Edge[]` JSON |
| `trace <id>` | Read-only | `{ root, nodes, edges }` JSON |
| `followups` | Read-only | `ArtifactMeta[]` JSON |
| `reindex` | Writes `search.db` | `{ indexed, skipped, removed, embedder }` JSON |
| `search <query>` | Read-only | `SearchResult[]` JSON (empty if no index) |
| `factory` | Read-only UI; `POST /api/open` spawns editor | Blocks until Ctrl-C |

All JSON commands write pretty-printed JSON to stdout. Graph and scheduling commands bootstrap the workspace and rebuild `ArtifactIndex` on each run (O(n) over artifacts).

## Reference graph: `refs`

### Add an explicit edge

```bash
loopany refs add --from <id> --to <id> --relation <verb>
```

<ParamField body="--from" type="string" required>
Source artifact id (slug).
</ParamField>

<ParamField body="--to" type="string" required>
Target artifact id.
</ParamField>

<ParamField body="--relation" type="string" required>
Open-registry relation verb (for example `led-to`, `addresses`, `mentions`).
</ParamField>

Appends one JSON line to `references.jsonl`:

```json
{"ts":"2026-06-05T12:00:00.000Z","from":"sig-q2","to":"tsk-ship","relation":"led-to","actor":"cli"}
```

Reverse edges are not stored; `ArtifactIndex` builds `refsIn` / `refsOut` maps at load time.

<RequestExample>

```bash
loopany refs add --from sig-q2 --to tsk-ship --relation led-to
```

</RequestExample>

<ResponseExample>

```json
{
  "ts": "2026-06-05T12:00:00.000Z",
  "from": "sig-q2",
  "to": "tsk-ship",
  "relation": "led-to",
  "actor": "cli"
}
```

</ResponseExample>

### Query neighbors (BFS)

```bash
loopany refs <id> [--direction in|out|both] [--relation R] [--depth N] [--domain D]
```

| Flag | Default | Behavior |
|------|---------|----------|
| `--direction` | `out` | Follow outgoing, incoming, or both edge sets per hop |
| `--depth` | `1` | Positive integer; N hops from the start id |
| `--relation` | (all) | Keep only edges matching this verb |
| `--domain` | (all) | Both endpoint artifacts must have `frontmatter.domain` equal to D |

Traversal is BFS over nodes. Each unique edge (`from|to|relation|ts`) is returned once even if reachable on multiple paths. Visited nodes are not re-expanded, but all edges encountered along the frontier are collected.

<Note>
Implicit edges appear in `refs` queries but are never written to `references.jsonl`. At index build time, `mentions` edges are synthesized from `frontmatter.mentions[]` and body `[[id]]` wiki links (`actor`: `frontmatter` or `body`, `implicit: true`). Updating frontmatter or body changes implicit edges on the next CLI run.
</Note>

## Causal lineage: `trace`

```bash
loopany trace <id> [--direction forward|backward|both] [--relations csv] [--max-depth N]
```

`trace` walks a **subset** of relations for lineage, not the full graph. Default predicates: `led-to`, `addresses`, `supersedes`, `follows-up`, `cites`. `mentions` is excluded (soft pointer, not lineage). `caused-by` is also excluded; use `--direction backward` on `led-to` edges to reach upstream work.

| Flag | Default | Behavior |
|------|---------|----------|
| `--direction` | `both` | `forward` follows `refsOut`; `backward` follows `refsIn` |
| `--relations` | causal set above | Comma-separated override; must list at least one verb |
| `--max-depth` | unlimited | Positive integer cap per direction |

<ResponseField name="root" type="string">
Start artifact id.
</ResponseField>

<ResponseField name="nodes" type="TraceNode[]">
Artifact metadata plus signed `distance`: negative = backward (causes), `0` = root, positive = forward (effects). Sorted by `distance`, then `id`. Dangling edge targets are dropped.
</ResponseField>

<ResponseField name="edges" type="Edge[]">
Edges used in the walk (deduped by `from|to|relation|ts`).
</ResponseField>

Cycle-safe: each node is assigned at most one distance; walks terminate on revisits.

<Warning>
`trace` requires the root id to exist. Unknown ids exit with `No artifact with id: …`.
</Warning>

<RequestExample>

```bash
loopany trace tsk-ship --direction both --max-depth 3
```

</RequestExample>

## Scheduling: `followups`

```bash
loopany followups [--due today|overdue|next-7d] [--include-done true] [--domain D]
```

Selects artifacts whose `checkAt` frontmatter (ISO date string) is on or before a cutoff:

| `--due` | Cutoff | Extra filter |
|---------|--------|--------------|
| `today` (default) | Today (UTC date portion) | None |
| `next-7d` | Today + 7 days | None |
| `overdue` | Today | Keeps only `checkAt` date **strictly before** today |

Unless `--include-done true`, results exclude artifacts in a **terminal** status: the kind’s status machine has no outgoing transitions from the current status (for example `done`, `cancelled`, `failed` on `task`). Kinds without a status machine are never filtered this way.

Output is JSON array of `ArtifactMeta` objects (`id`, `kind`, `path`, `frontmatter`) — same shape as `artifact list`, filtered by schedule.

<Steps>

<Step title="Daily review">
Run `loopany followups --due today` (optionally `--domain crm`) to list actionable `checkAt` items.
</Step>

<Step title="Overdue sweep">
Run `loopany followups --due overdue` for items past their date that are still non-terminal.
</Step>

<Step title="Weekly horizon">
Run `loopany followups --due next-7d` to see the next week’s scheduled reviews.
</Step>

</Steps>

## Hybrid search

### Rebuild index: `reindex`

```bash
loopany reindex [--force] [--no-embed]
```

| Flag | Effect |
|------|--------|
| `--force` | Deletes `search.db` and rebuilds FTS5 + chunk tables from scratch |
| `--no-embed` | Uses `NoopEmbedder` — FTS5-only index (no ONNX download; used in CI) |

Incremental by default: compares each artifact file’s mtime to `artifact_mtimes` in `search.db` and skips unchanged files. After scanning disk, removes index rows for artifact ids no longer present.

<Info>
Artifact create/append/status does **not** auto-reindex in v1. Run `reindex` after bulk edits or when you want fresh embeddings.
</Info>

<ResponseExample>

```json
{
  "indexed": 12,
  "skipped": 45,
  "removed": 1,
  "embedder": "transformers"
}
```

</ResponseExample>

`embedder` is `transformers` when `Xenova/all-MiniLM-L6-v2` loads, otherwise `noop` (keyword-only search still works).

Indexing pipeline (`SearchStore`):

- Chunks: optional title chunk + body split at `##` / `###` headings
- Keyword: SQLite FTS5 (`chunks_fts`) with BM25 rank
- Semantic: 384-dim embeddings when embedder is available; cosine similarity ≥ `0.3`
- Fusion: Reciprocal Rank Fusion (`k = 60`) across keyword and semantic ranked lists; best chunk per artifact wins

Derived file: `$LOOPANY_HOME/search.db` (safe to delete; markdown artifacts remain canonical).

### Query: `search`

```bash
loopany search <query> [--kind K] [--domain D] [--status S] [--limit N]
```

| Flag | Default |
|------|---------|
| `--limit` | `10` |

If `search.db` is missing, stdout is `[]`, stderr prints `no search index found — run loopany reindex first.`, exit code `0` (scripts can probe without failing).

<ResponseField name="artifactId" type="string">
Matched artifact id.
</ResponseField>

<ResponseField name="snippet" type="string">
Truncated chunk text (~200 chars).
</ResponseField>

<ResponseField name="score" type="number">
RRF fused score (higher is better).
</ResponseField>

Additional fields: `kind`, `domain`, `status`, `path`, `section`.

<RequestExample>

```bash
loopany reindex
loopany search "billing refactor" --kind note --limit 5
```

</RequestExample>

<Tip>
Use `loopany reindex --no-embed` in automation or offline environments; use a full `reindex` (no flag) when semantic recall matters and the Hugging Face model can download to `HF_HOME`.
</Tip>

## Factory UI: `factory`

```bash
loopany factory [--port N] [--host H] [--no-open]
```

| Flag | Default |
|------|---------|
| `--port` | `4242` |
| `--host` | `127.0.0.1` |
| `--no-open` | (browser opens via `open` / `xdg-open` / `start`) |

Starts a local Bun HTTP server (`src/ui/server.ts`) serving a read-only Kaplay pixel view. Binds to loopback only — no auth, no CORS.

| Route | Method | Purpose |
|-------|--------|---------|
| `/` | GET | Factory HTML + game |
| `/api/graph` | GET | Workspace graph JSON from `buildGraph()` |
| `/api/open` | POST | `{ "id": "<artifact>" }` → opens artifact path in editor |

Graph payload includes `nodes`, `edges` (explicit + implicit), `lanes` (kind ordering), `domains`, `enabledDomains`, `workspace`. Nodes carry card fields and a short body preview, not full markdown.

Editor resolution for `/api/open`:

1. `$LOOPANY_EDITOR` (shell-split command, e.g. `cursor` or `code -n`)
2. Platform default: `open` (macOS), `xdg-open` (Linux), `start` (Windows)

Process runs until SIGINT/SIGTERM; then the server stops.

## `audit.jsonl` side effects

Every CLI invocation (including read-only graph/search queries) triggers a best-effort append to `$LOOPANY_HOME/audit.jsonl` after the command finishes. Audit failures are silent and never change the command exit code.

Row shape:

```json
{
  "ts": "2026-06-05T12:00:00.000Z",
  "op": "refs.add",
  "actor": "cli",
  "duration_ms": 42,
  "from": "sig-q2",
  "to": "tsk-ship",
  "relation": "led-to"
}
```

`op` is the command key with spaces as dots (`refs`, `refs.add`, `trace`, `followups`, `search`, `reindex`, `factory`). Failed runs add `"error": "<message>"`. Large fields (artifact bodies) are never logged.

| Command | Typical audit fields |
|---------|---------------------|
| `refs add` | `from`, `to`, `relation` |
| `refs` | `count` |
| `trace` | `nodes`, `edges` |
| `followups` | `count` |
| `search` | `count` |
| `reindex` | `indexed`, `skipped`, `removed`, `embedder` |
| `factory` | `op` only (duration spans entire session) |

`artifact get <id> --format json` attaches filtered audit history for that id (`artifact.create`, `artifact.append`, `artifact.status`, `artifact.set`, `refs.add` where `id`, `from`, or `to` matches).

<Check>
Verify audit growth: run a command, then `tail -1 $LOOPANY_HOME/audit.jsonl | jq .op`.
</Check>

## `refs` vs `trace`

| | `refs <id>` | `trace <id>` |
|---|-------------|--------------|
| Relations | All (unless `--relation`) | Default causal set; `mentions` excluded |
| Implicit edges | Included | Included if relation filter allows |
| Output | Flat `Edge[]` | Nodes with signed `distance` + edges |
| Use case | Neighborhood / degree-N graph | Cause → root → effect narrative |

## Errors and recovery

| Symptom | Cause | Recovery |
|---------|-------|----------|
| `refs add requires --from, --to, --relation` | Missing flags | All flags require values (`--relation led-to`) |
| `Invalid --depth` / `--max-depth` | Non-positive integer | Pass `1` or greater |
| `No artifact with id` on `trace` | Unknown root | Confirm id via `artifact list` |
| Empty `search` + stderr hint | No `search.db` | `loopany reindex` |
| `embedder: "noop"` after reindex | Model load failed | Check stderr during reindex; FTS-only still works |
| Stale search hits | Index not refreshed | `reindex` (or `reindex --force` after model change) |

## Related pages

<CardGroup>

<Card title="Reference graph" href="/reference-graph">
Append-only edges, implicit mentions, relation conventions, and wiki-link rules.
</Card>

<Card title="CLI reference" href="/cli-reference">
Full command surface and JSON stdout conventions.
</Card>

<Card title="Artifacts and workspace" href="/artifacts-and-workspace">
`references.jsonl`, `search.db`, `audit.jsonl`, and on-disk layout.
</Card>

<Card title="Periodic review" href="/periodic-review">
Cadence for `followups --due today` and overdue sweeps.
</Card>

<Card title="Doctor and troubleshooting" href="/doctor-and-troubleshooting">
Index health, dangling refs, and factory/reindex recovery.
</Card>

</CardGroup>
