# f0rge schema v1 (frozen)

Single source of truth for every `.forge/` format. No byte changes
without a schema-version bump in the contract (`f0rge --skill`).

## 0. Global rules

- Identity: `id` is uuid4, lowercase, dashes kept. Never changes.
- Timestamps: RFC3339 UTC with fractional seconds: `2025-09-08T02:00:00.123456Z` (parser accepts plain `...00Z` too).
  Lexicographic order == chronological order. Field names: `created`,
  `updated`, `closed`, `due` (milestones: `YYYY-MM-DD` or full RFC3339).
- Files: YAML frontmatter between `---` fences + markdown body.
  No frontmatter (except `ci.env`, `labels.md`, `milestones.md`,
  scripts) = error. Unknown frontmatter field = warning, never error.
  Missing required field = error naming file + field.
- Parse caps (SEC-01): file > 256 KiB rejected; nesting depth > 32
  rejected; > 100 YAML aliases rejected. All before full parse.
- Numbers: derived, never stored. Issue/PR `number` = 1-based rank of
  `id` in total order `(created, id)` at list time.
- Archive: `state: archived` + move to `.forge/archive/<type>/<uuid>/`.
  Nothing is deleted. Indexes skip archived by default.
- CI verdict vocabulary: `pass | fail | running | cancelled`.
- Gitignored: `.forge/ci/runs/`, `.forge/releases/out/` only.
- States: issue `open|in-progress|closed|archived`;
  pr `open|closed|merged` (+ `draft:` bool);
  task `status: todo|doing|blocked|done`;
  milestone `open|closed`.
- address: issue <N> | task|pr <uuid> | milestone|label <name> | wiki <path>

## 1. Layout (OUTLINE §5)

```
.forge/issues/<uuid>/issue.md + comments/<uuid>.md
.forge/prs/<uuid>/pr.md + patch.diff + comments/<uuid>.md + reviews/<uuid>.md
.forge/tasks/<uuid>.md
.forge/labels.md · milestones.md
.forge/wiki/ (index derived: tree + first H1)
.forge/ci/ci.env + steps/NN_name.sh + runs/<ts>/summary.md
.forge/releases/<version>.md + build/ + out/ (ignored)
.forge/merges/<ts>.md
.forge/archive/<type>/<uuid>/
```

## 2. Entities

### issue — `.forge/issues/<uuid>/issue.md`
Required: `id`, `title`, `state`, `created`.
Optional: `labels: [str]` ([]), `assignees: [str]` ([]),
`milestone: str`, `updated`. Body = issue text.
Golden: `spec/fixtures/issue.md`.

### issue-comment — `comments/<uuid>.md`
Required: `id`, `created`. Optional: `author`. Body = comment.
Golden: `spec/fixtures/issue-comment.md`.

### pr — `.forge/prs/<uuid>/pr.md`
Required: `id`, `title`, `state`, `base`, `head`, `created`.
Optional: `draft: bool` (false), `issues: [uuid]` ([]),
`ci: verdict`, `updated`. Body = description; sidecar `patch.diff`.
Golden: `spec/fixtures/pr.md`.

### pr-review — `reviews/<uuid>.md`
Required: `id`, `created`.
Optional: `author`, `state: approved|changes-requested|commented`.
Body = review text. Golden: `spec/fixtures/pr-review.md`.

### task — `.forge/tasks/<uuid>.md`
Required: `id`, `title`, `status`, `created`.
Optional: `assignee`, `updated`, `closed`.
Column derived from `status` (`todo|doing|blocked|done`).
Create: `--title T [--body B] [--column C]` (default `todo`); body = markdown after frontmatter. Golden: `spec/fixtures/task.md`.

### label — entries in `.forge/labels.md` (YAML list)
Required: `name`. Optional: `color`, `description`.
Golden: `spec/fixtures/labels.md`.

### milestone — entries in `.forge/milestones.md` (YAML list)
Required: `name`. Optional: `state` (open),
`due`, `description`. Golden: `spec/fixtures/milestones.md`.

### release — `.forge/releases/<version>.md`
Required: `version`, `created`. Optional: `title`, `sha` (HEAD short sha at create time; absent on old records).
Body = notes. Verbs: `create <ver> --notes <file|-> [--title S]`,
`install [ver|main] [--dry-run]` (list/view unchanged). Golden: `spec/fixtures/release.md`.

### merge-record — `.forge/merges/<ts>.md`
Required: `pr` (uuid), `verdict: pass|fail`, `created`.
Optional: `applied: bool` (false), `resolved: [str]` ([]).
Body = notes. Golden: `spec/fixtures/merge-record.md`.
Gate only: CI-gates patch in scratch worktree; never touches main/branches; prints conflicts, never resolves; you integrate (ff) + delete branch.

### ci-env — `.forge/ci/ci.env` (YAML mapping, no fences)
Known: `runner: local|docker` (default local).
Any extra `KEY: value` string knob allowed, never secret.
Golden: `spec/fixtures/ci-env`.

### ci-step — `.forge/ci/steps/NN_name.sh`
Executable script, filename order, exit 0 = step pass,
first nonzero exit = run `fail`. Golden: `spec/fixtures/ci-step.sh`.

### ci-run — `.forge/ci/runs/<ts>/summary.md` (gitignored)
Required: `run`, `verdict`, `created`.
Optional: `steps: [{name, exit, duration_ms}]`.
Golden: `spec/fixtures/ci-run.md`.

## 3. Derived indexes (pure, deterministic)

- Issue/PR numbers: sort `(created, id)`, 1-based rank.
- Kanban: group task ids by `status` (`todo|doing|blocked|done`).
- Wiki index: walk tree, title = first `# H1` else filename.
- `--json` (compact, fixed order):
  issue `number,title,state,labels,assignees,milestone,body,createdAt,comments`
  (comments = count); pr `number,title,state,base,head,draft,
  databaseId,status,conclusion,createdAt`
  (`databaseId` = uuid, `status`/`conclusion` from `ci`);
  wiki list `path,title,size`; release create/view
  `version,title,created,body,status`, release list
  `version,title,created,status` (`status`: `built|none`, derived from
  `out/<ver>/`).

## 4. Procedures (wiki, release, init)

### wiki — files are the API; index derived, never stored
- `wiki get <path>`, `wiki put <path> -` (stdin only), `wiki list`.
- Index: walk tree (symlinks skipped, files > 256 KiB skipped),
  index title = first H1, else filename; sorted by path.
- Paths are wiki-relative: absolute, `.`, `..` rejected
  (`escapes wiki`, exit 1); missing page (`not found`, exit 1);
  directories are not pages (`is a directory`, exit 1).
- Put/get are exact bytes; stdin capped at 256 KiB.
- wiki pages are plain files in git; edit or remove with rm + git (git is the undo log); 'never delete' governs entity history (issues/prs/tasks), not doc files.

### release — notes are truth, build is a side effect
- `release create <ver> --notes <file|-> [--title <t>]` writes
  `releases/<ver>.md` (upsert: last write wins), then runs the build.
- `<ver>` becomes `<ver>.md`; separators/escapes rejected
  (`escapes releases`, exit 1); missing `--notes` is usage (exit 2).
- Build: executable scripts in `releases/build/` (filename order,
  argv-only, `VERSION`/`OUT` env) run at the `<ver>` tag in a temp
  worktree, else against the current tree; outputs land in
  `releases/out/<ver>/` (gitignored); sha256 of artifacts is appended
  to the notes body. Empty build dir = no-op.
- `release list` order: `(created, version)`; `release view <ver>`
  prints the notes body. No delete verb (nothing is deleted).
- Install (local machine op; never forge state, never CI):
  `release install` (latest record), `release install <ver>` (pinned:
  record must exist else `no such release: <ver>`, exit 2),
  `release install main` (current checkout). Resolve record sha >
  git tag > current checkout for pin + stage `releases/build/<ver>/`; if
  CWD repo-root `install.sh` exists run it argv-only as `./install.sh
  <ver>` (F0RGE_RELEASE_SHA env carries the pin; it owns placement); else if
  CWD `Cargo.toml` exists `cargo install --locked` from the resolved tree; else `no installer for
  this project (install.sh or Cargo.toml)` (exit 1). `--dry-run`
  prints the path it would take (one line) without touching the machine.
- Build artifact: CI copies `target/release/f0rge` to
  `releases/build/<ver>/f0rge` per release record (gitignored,
  never committed); `status: built|none` still derives from
  `out/<ver>/`.

### init — bootstrap skeleton, idempotent
- `init [dir]` (default cwd) creates dirs `issues prs tasks
  wiki/docs wiki/research wiki/experiments ci/steps releases/build
  merges`, files `labels.md` (`[]`), `milestones.md` (`[]`),
  `ci/ci.env` (`runner: local`), and appends any missing `.gitignore`
  lines (`.forge/ci/runs/`, `.forge/releases/out/`, `*.tmp`).
- Idempotent: existing files kept (never clobbered), gitignore lines
  never duplicated. No git requirement (works in any dir).
