# Contributing

Thanks for taking the time to contribute to `oh-my-knowledge`.

## Branch model — GitHub Flow

One long-lived branch:

- **`main`** — the single integration and release branch. All production-ready work lands here through PRs. Do not commit directly to `main`.

All work happens on short-lived topic branches cut from `main`:

| Branch prefix | Cut from | Merges back to | Purpose |
|---|---|---|---|
| `feat/<desc>` | `main` | `main` | new feature |
| `fix/<desc>` | `main` | `main` | bug fix, including urgent production fixes |
| `docs/<desc>` | `main` | `main` | docs only |
| `chore/<desc>` | `main` | `main` | build, tooling, dependency bumps, release version bumps |

Delete the topic branch after its PR is merged.

## Typical workflow

### Prerequisites

This repo uses **Yarn 4** (Berry), pinned via the `packageManager` field and driven by [Corepack](https://nodejs.org/api/corepack.html). Node ships Corepack but leaves it disabled, so enable it once:

```bash
corepack enable    # makes `yarn` in this repo resolve to the pinned Yarn 4
```

Node ≥ 22 is required (`engines`). After that, the `yarn` commands below Just Work — Corepack downloads the pinned Yarn version on first use.

### Everyday feature / fix

```bash
# sync main first
git checkout main
git pull --ff-only

# cut a topic branch from main
git checkout -b feat/my-feature

# build / test / lint
yarn install
yarn build
yarn test      # must pass
yarn lint      # must pass

git commit -m "feat(cli): 中文 subject"
git push -u origin feat/my-feature

# open a PR against **main**
```

### Releasing a new version

```bash
# cut a release version-bump branch from main
git checkout main
git pull --ff-only
git checkout -b chore/release-0.28.0

# bump version in package.json, final polish commits, then verify
yarn lint
yarn build
yarn test

# commit and open a PR against main
git commit -m "chore(release): 发布 0.28.0"
git push -u origin chore/release-0.28.0

# after the PR is merged, tag the merge commit on main
git checkout main
git pull --ff-only
git tag -a v0.28.0 -m "Release v0.28.0"

# Push the tag as its own command.
# `git push origin main --tags` is an atomic push — GitHub treats it as one
# event and only triggers workflows watching `branches: [main]`, so the
# `tags: ['v*']` trigger in publish.yml never fires and npm publish is
# skipped. Pushing the tag in its own command produces a distinct push
# event that fires the tag workflow.
git push origin v0.28.0
```

### Hotfix against a released version

```bash
# cut from main
git checkout main
git pull --ff-only
git checkout -b fix/critical-bug

# make the fix, bump patch version, verify, then open a PR against main
yarn lint
yarn build
yarn test
git commit -m "fix(cli): 中文 subject"
git push -u origin fix/critical-bug

# after the PR is merged, tag the merge commit on main and push the tag
git checkout main
git pull --ff-only
git tag -a v0.28.1 -m "Release v0.28.1"
git push origin v0.28.1
```

## Release notes

Release notes are auto-generated by GitHub from merged PRs (no separate `CHANGELOG.md` to maintain). For BREAKING / migration callouts to surface clearly, prefix the PR title with the relevant tag — e.g. `feat(judge)!: BREAKING-COMPARABILITY ...` — and put migration steps in the PR description. The `softprops/action-gh-release` step in `.github/workflows/publish.yml` runs with `generate_release_notes: true` and produces the GitHub Release body from the PR list between tags.

After publish.yml finishes, **review the auto-generated GitHub Release notes** at `https://github.com/lizhiyao/oh-my-knowledge/releases/tag/vX.Y.Z`. The auto-gen lists PR titles only — if any included PR was tagged `BREAKING-*`, the migration steps from that PR's description need to be hoisted into the release notes by hand:

```bash
gh release edit vX.Y.Z --notes "$(cat <<'EOF'
## ⚠️ Breaking changes

<consolidated migration guide pulled from BREAKING-* PR descriptions>

---

<paste the auto-generated body below>
EOF
)"
```

This keeps users who only read release notes (not individual PR descriptions) from missing the migration path.

## Commit messages

Use Conventional Commits with a stable scope and a Chinese subject:

```
feat(cli): 新增工作流命令
fix(eval-core): 修复事实检查误报
docs(readme): 补充评测用例说明
```

## Tests

- `yarn test` runs the full vitest suite
- Add tests for behaviour you change; a regression test for bug fixes is strongly preferred
- CI runs the same commands on Node 22 and Node 24 for `main` pushes and PRs targeting `main` — all must pass before merge

## CLI 走 oclif 框架(issue #109 / #121)

omk CLI 走 [@oclif/core](https://oclif.io/docs/) 框架，**single parse path**:oclif Command 一次 typed parse(`await this.parse(X)`)拿 `{args, flags}` 直接喂业务。无 legacy 二次解析、无 `execute(this.argv)` 透传。

`src/cli/` 三段式布局:

| 目录 | 职责 | 内容 |
|---|---|---|
| `commands/` | 产品 CLI 命令树 | 每个 file = 一个 oclif Command 类 + 业务 inline (top-level + observe / eval/gold sub) |
| `lib/` | 跨命令共享 utility | `cli-exit.ts` / `cmd-flags.ts`(typed args/flags interface) / `i18n.ts` + `i18n-dict.ts`(tCli + 业务消息字典) / `parse-run-config.ts` / `progress.ts` / `run-tally.ts` / `shared.ts`(report helpers) / `update-check.ts` |
| `oclif/` | 框架 plumbing | `help.ts`(LangAwareHelp) / `projection.ts` / `i18n.ts`(bilingual / pickLang / resolveLang) / `run.ts`(dispatcher entry) |

文件目录约定:

- `src/cli/commands/doctor.ts` → `omk doctor`(扁平命令直接放 file)
- `src/cli/commands/eval/index.ts` + `eval/gold/{init,validate,compare}.ts` → `omk eval` / `omk eval gold *`(有 sub 的进同名 directory,`index.ts` 是 default command,oclif file routing 约定)
- `src/cli/commands/observe/index.ts` + `observe/{ingest,inbox,show}.ts` → `omk observe` / `omk observe *`

业务住哪儿:

- 简单业务 inline 到 `Command.run()` body
- 测试需要 in-process import 验证的业务,作为 module-level helper(如 `runStudio` / `runEvolve` / `runObserveInbox` / 3 个 sample-fix helper)export from 同一个 Command file
- 跨命令共享的 utility 进 `src/cli/lib/`;跟 CLI entry 解耦的 deep functions 在自己的领域目录(如 `src/doctor/index.ts:runDoctor`)

双语 help 走 `src/cli/oclif/i18n.ts` 的 `bilingual({zh, en})` + `src/cli/oclif/help.ts` 的 `LangAwareHelp` 子类,按 `--lang` / `OMK_LANG` 在渲染时切语言。每个 flag 的双语 description inline 写,不进 `lib/i18n-dict.ts`(那份只给 runtime `cli.error.*` / `cli.gen.*` 等业务消息用)。lang 解析 source-of-truth 是 `resolveLang(process.argv)`(scan raw argv + env fallback);**不要**读 `flags.lang`(oclif `default: 'zh'` 会盖掉 `OMK_LANG=en`)。

注意:`oclif/i18n.ts` 跟 `lib/i18n.ts` 职责不同 — 前者是 oclif Command static 字段需要的双语 sentinel + LangAwareHelp 渲染入口,后者是 runtime tCli / tBoth / getCliLang / parseLangFromArgv 基础设施。两者不要合并(reviewer 已确认强行扁平反而混)。

加新命令的步骤:

1. 在 `src/cli/commands/<name>.ts` 写 `export default class extends Command`,声明 `static args / flags / examples / description`(flag description 用 `bilingual({zh, en})` 包装)
2. `run()` 体里:`const { args, flags } = await this.parse(<Class>); const lang = resolveLang(process.argv);`,然后业务 inline 或调 module-level helper
3. CliExit 边界:业务 `throw new CliExit(code)` 通过每个 Command 共用模板捕获 → `this.exit(code)`(模板见现有 commands 任一 `run()` 末尾的 try/catch)
4. 在 `test/cli/oclif-<name>.test.ts` 加 --help 双语 + unknown flag exit 2 + 关键 happy/error case
5. 跑 `yarn build && yarn build:docs` 把 oclif Command 的 description / flags / examples 同步到 `.agents/skills/omk/references/commands.md`（见下一节）

### CLI 文档 codegen（#109）

oclif Command 的 `description` / `flags` / `args` / `examples` static 字段是 CLI 文档的**单一来源**。`scripts/build-docs.ts` 把它渲染到三个目标文件的 marker 区段:

| 目标 | marker | 输出 | 语言 |
|---|---|---|---|
| `.agents/skills/omk/references/commands.md` | 整段 `<!-- omk:cli:start -->` ... `<!-- omk:cli:end -->` | oclif `Config.commands` 全集完整渲染（含 topic / sub / sub-sub） | zh |
| `docs/reference/cli.md` | 每个顶层命令独立 `<!-- omk:cli:<id>:flags:start -->` ... `<!-- omk:cli:<id>:flags:end -->` | flag list（```text``` 对齐风格）+ 指向 `--help` 的脚注 | en |
| `docs/zh/reference/cli.md` | 同上 | 同上 | zh |

`SKILL.md` 不走 codegen（agent prompt 指令塞结构化命令清单跟 commands.md 重复,还撑大 agent context）。改用 `test/scripts/build-docs.test.ts` 的 vitest case 锁 frontmatter `argument-hint` 跟 oclif 顶层命令 id set（`TOP_LEVEL_IDS`）严格一致——历史上漂过 2 次（`bench run` → `eval`、`improve` → `evolve`),这条 test 把同类 drift 拦在 CI。

工作流:

- 改完 oclif Command 的 description / flag,跑 `yarn build && yarn build:docs` 同步全部 3 个目标
- 不跑就会被 vitest 内嵌 `--check` 拦截（exit 1 + 对每个 drift 的文件打 diff）
- CLI reference 的 prose 段（static-only 解释 / HTML report tab / Studio IA / executor 表格等）在 marker 外,hand-maintained 保留
- 新增顶层命令时:加 `src/cli/commands/<id>.ts`、在 `docs/reference/cli.md` / `docs/zh/reference/cli.md` 各加一对 `<!-- omk:cli:<id>:flags:start -->` / `:end -->`、SKILL.md frontmatter `argument-hint` 加 `<id>`,跑一遍 `yarn build && yarn build:docs && yarn test`(顶层命令集真值由 `scripts/build-docs.ts` 的 `getTopLevelIds(Config.load)` 从 oclif Command 文件目录派生,不需要再单独维护硬编码数组)
- 新增子命令（如 `omk eval gold init` 这种 sub-sub）时:加 `src/cli/commands/eval/gold/init.ts`,oclif 文件目录自动路由,fullbody 模式自动包含

### 加新 sub-sub topic 命令

目录下有 sub-sub 但目录本身没 default Command 时(如 `eval/gold/` 下有 `init` / `validate` / `compare`,目录本身 `eval gold` 不直接执行),**必须** 加 `<dir>.ts` 表达 topic semantics:

- 当前实例:`src/cli/commands/eval/gold/index.ts` — 裸 `omk eval gold` 打 usage + `this.exit(1)`,跟 legacy 行为(missing sub-sub → CliExit(1))一致
- 不加的代价:oclif 默认把 `eval gold` 当 topic-only,裸调用落到 default topic help(exit 0),CI 脚本如果靠 exit 1 区分「用户漏写 sub-sub」会失效
- 当前 omk 只有 eval gold 一处需要,加新 sub-sub 目录时遵循

### oclif description ejs footgun

oclif Help 用 `ejs.render(body, context)` 渲染所有 section,模板标记 `<%...%>` 会被执行成代码。**flag / arg / description 字段不要拼用户输入(skill 名、文件路径、env 值)**;模板只在 `examples[].command` 字段使用(by-design,如 `'<%= config.bin %>' init`)。

`src/cli/oclif/i18n.ts` 的 `bilingual({zh, en})` 已经在入口加 assertion 拦 `<%` / `%>`,description 里写模板会在 runtime 启动时抛错。

### CLI exit code 约定

oclif 迁移后 omk 的 exit code 契约（CI / 脚本若有 `[ $? -eq N ]` 分支按此判断）:

| code | 触发场景 |
|---|---|
| `0` | 正常完成 |
| `1` | 业务失败（doctor 门禁拒绝、verdict REGRESSION、未知命令、缺 required positional —— 走生产逻辑或 oclif `default` exit code）|
| `2` | flag / arg 校验失败（未知 flag、missing required arg、type mismatch —— 由 oclif `failedFlagParsing` / `requiredArgs` / `nonExistentFlag` 等捕获,见 `package.json:oclif.exitCodes`）|

迁 oclif 前 `omk evolve`（missing skillPath）跟 `omk observe ingest`（missing traceDir）走生产 `throw new CliExit(1)`,迁后 oclif `Args.required: true` 走 `requiredArgs: 2`。脚本如果靠 `1` 区分 user error vs parse error,要按上表更新。

## Style

- TypeScript strict; `yarn lint` must be clean
- No unnecessary abstractions; no code for hypothetical future needs
- Keep comments minimal — explain the *why*, not the *what*

## Docs layout

`docs/` is organized by audience — pick the right bucket before creating a new file:

- `docs/reference/` — flag tables, sample format, executor matrix, comparison ↔ what users look up
- `docs/explanation/` — architecture, statistical rigor ↔ how / why omk works
- `docs/specs/` — design specs read by contributors (sample design, gap signal, RAG metrics, terminology, ...)
- `docs/quickstart-skill-eval.md` + `docs/README.md` stay at the top level; `docs/zh/` mirrors the same structure
- `docs/README.md` + `docs/zh/README.md` are the audience-grouped indexes — add new files to the matching section
- **EN/ZH symmetry is required.** Every published page has both an English version under `docs/` and a Chinese version at the same path under `docs/zh/` — the VitePress language switcher relies on this 1:1 mirror, and a strict dead-link build fails otherwise. When adding a doc, add both halves. Prefer putting design rationale and decision records *inside* the relevant published spec (e.g. as an appendix) so users see the full reasoning — keep notes outside `docs/` only when they are genuinely maintainer-only, where they stay exempt from the bilingual rule

## Scope

`oh-my-knowledge` focuses on **offline knowledge-artifact evaluation** and **skill-level production observability**. It deliberately does **not** cover:

- General APM / request-level latency and cost tracing (use Langfuse / Datadog)
- Streaming evaluation / real-time alerting
- Production scoring (no control group = no score)

PRs expanding into those areas will usually be declined. If you're unsure whether your idea fits, open a GitHub Discussion or issue first.

## Security

See the [Security notice](./README.md#security-notice) in the README for risks around custom assertions and the local report server.
