Files
review/skills/api-doc/SKILL.md
T
jiantw83 d472f81cd2 feat(狀態回報): 收尾寫一筆 skill-end 事件
現行紀錄只記「被叫用」,沒有成敗也沒有結束碼。跑完整輪的技能與開場就
中止的技能,在紀錄裡長得一模一樣。

start 由技能用量 hook 順手發,不必改技能文件。end 只能由技能自己在收尾
步驟寫——hook 接在技能工具呼叫上,而實際工作發生在之後的模型輪次,它在
原理上看不到成敗。有 start 沒有配對的 end,就是那一輪中止了。

status 五選一,每支技能各自寫明什麼情況選哪一個。找不到回報腳本就安靜
跳過,回報失敗一律不改變技能自己的結論。
2026-09-02 16:01:17 +08:00

97 lines
15 KiB
Markdown

---
name: api-doc
description: 'Audit an API project''s Swagger/OpenAPI documentation: every returnable HTTP status code declares a response type, every input and output carries a description, and a real data example sits on scalar members only — a data-model member takes a description and hands the example duty down to its own properties, recursively to the innermost scalar. Run after controller work or an implementation is complete, e.g. from jsc-sdlc implement, next to jsc-review:code-review. Detection runs first via tools/swagger-detect.sh; without both the Swagger package and its wiring, report unsupported and stop. The source comment contract belongs to jsc-review:code-review group 5; this skill covers Swagger document attributes only. Findings carry file:line, severity, and the fix; this skill never modifies code.'
---
# api-doc
Audit whether an API project's Swagger (OpenAPI) documentation is complete enough for a caller to integrate against it without reading the implementation.
## When to run
1. **After controller work is complete**: one controller or one related group of controllers is done.
2. **When an implementation is complete**: all todos of a work package that touched API endpoints are done. This is the call site in `jsc-sdlc:implement`, right next to `jsc-review:code-review`.
3. **Never on a project without Swagger**: step 1 below decides this in code, not by judgment.
## Division of labor
- This skill covers **Swagger document attributes only**: response type declarations, Swagger parameter descriptions, and Swagger examples.
- The comment contract — method description, layer tag, parameter description, return description, examples, nested-structure comments — belongs to `jsc-review:code-review` group 5 (`references/smells.md` 5.1 to 5.8). Never report the same gap twice; when a nested model already fails 5.5, leave it to `code-review`.
- Security, logic bugs, and test coverage belong to the CLI's built-in review.
## Steps
1. Detect Swagger support: run `tools/swagger-detect.sh {project path}` (defaults to the current directory). The script confirms package **and** wiring, so an installed-but-never-enabled project comes back unsupported. Read its exit code: `0` supported; `1` unsupported; `2` one of three causes — more than one argument, a project path that does not exist, or no `grep` in the environment. Completion condition: the exit code and the `support=`, `stack=`, `package=`, `config=` lines are captured.
2. On exit code `1`, report the literal 「本專案未啟用 Swagger 文件,略過 API 文件稽核」 plus the `stack=` and `package=` lines the script printed, and stop. Spawn no sub agent. On exit code `2`, report the script's stderr message verbatim, then route by which of the three causes that message names: the usage line means the call passed more than one argument, so re-run with at most one path; 「找不到專案路徑」 means the caller supplies an existing project path and re-runs; 「環境缺 grep」 means the environment itself is broken, so report that `grep` must be installed and stop — re-running with another path never clears this one. Spawn no sub agent in any of the three. Completion condition: the run has ended with the cause named and the matching next action stated, or exit code `0` moved it to step 3.
3. List the audit scope: every controller in the project, or only the controllers touched by `git diff` when the caller asked for a scoped run. On a scoped run, read git's exit code: `0` the changed controllers are the scope; any non-zero code means git refused (not a repository, unknown base revision, unreadable object) — report git's stderr verbatim and stop, spawn no sub agent. Never fall back to the whole project there: the caller asked for one scope, and quietly auditing a much larger one buries the git failure under a far longer run. `jsc-review:code-review` step 1 stops on the same failure, so both skills answer a broken `git diff` the same way. Completion condition: the controller list is non-empty and reported; an empty list ends the run with the literal 「無發現」, and a git failure ends it with git's error.
4. Audit in two aspects, and every aspect **MUST run as a sub agent**; the two may run in parallel:
| Aspect | Scope |
| --- | --- |
| 1 Status codes | For every action, every HTTP status code it can actually return — success, validation failure, authorization failure, not found, server error — has a declared response type and body schema (`ProducesResponseType`, `@ApiResponse`, FastAPI `responses=`, drf-spectacular `@extend_schema`). A status code the code can produce but the document never declares is a finding; so is a declared status code the code can never produce |
| 2 Description and example, down every data model | Two checklists over one pass of the controller's input and output types. Checklist A — description and example: every input parameter and every output field has a description, whatever its type, and the example requirement follows the type. A scalar member — string, number, boolean, date, enum — has an example as well. A data-model member has no example of its own; it keeps the description and hands the example duty down to its properties, where checklist B collects it. A collection is judged by its element type: scalar elements make the collection scalar, so it carries one example listing a couple of elements; model elements make the collection a data model, so it carries a description only and the walk enters the element type. A dictionary is judged by its value type on the same rule. Examples come from real project data first (the project's database, seed data, or fixtures), and only when real data is unreachable is an example derived by logic and marked as a derived value in the document itself. Checklist B — recursive walk: when a parameter or return value is a data model, or a collection of one, checklist A applies to every one of its properties, and a model containing another model recurses to the innermost layer. Every scalar property on every layer carries a description and an example; every intermediate model property carries a description only. Walk in from the controller, follow the input and output types down, and end each branch on the layer whose properties are all scalar. Run checklist A on each member as the walk in checklist B reaches it, so each data model file is opened once and both checklists are answered for it |
Checklists A and B ran as two sub agents before, and they opened the same data model files twice. Aspect 2 is one sub agent now. The checklists stay listed apart so no check goes vague once they share a pass: a missing description or example is reported under A, a member the recursive walk never reached is reported under B, and field 4 of every row says which one.
An example exists so the caller reads the literal payload straight off the document, and every literal value has exactly one owner. That is why a data-model member carries none: its payload is defined by its properties' own examples, and a second copy on the parent goes stale the day a property changes. A collection follows the same test on its element type — a list of scalars fits in one literal, so the example is complete where it stands, while a list of models only repeats what the element model already owns. Judge the element type, never the collection wrapper, so `List<string>` and `string` come out the same and `List<Address>` and `Address` come out the same.
Instructions for each sub agent: read only, change nothing. Return findings as TSV rows — one finding per row, six fields separated by a single tab, in this order:
| Field | Content |
| --- | --- |
| 1 file | path relative to the repository root |
| 2 line | line number, a positive integer |
| 3 severity | `高`, `中`, or `低` on the scale below |
| 4 group | the aspect: `1` status codes, `2A` description and example, `2B` recursive walk |
| 5 item | the finding name, taken from the Severity table below |
| 6 detail | one sentence of evidence plus the concrete fix — which attribute to add, on which member |
Fields 5 and 6 are written in Traditional Chinese. Free-form prose is not accepted: `tools/merge-findings.sh` deduplicates on fields 1 and 2, so any row off this format is rejected and its finding never reaches the report. An aspect with nothing to report returns the single line 「無發現」 instead of rows.
Completion condition: both aspects have returned, each with TSV rows or with 「無發現」.
5. Merge with `tools/merge-findings.sh`. Concatenate the two aspects' TSV rows in aspect order and pipe them into the script on stdin. Start this step only once both have returned. Read its exit code: `0` take its stdout as the merged list — deduplicated by location and sorted by severity, 高 first and 低 last; `1` no aspect returned a row, so report the literal 「無發現」 and stop; `2` environment or input error, so report the script's stderr verbatim and stop, and never hand-merge as a substitute; `3` an aspect returned a malformed row, and the stderr names which row, so ask that one aspect's sub agent to re-emit in the format above and run the merge again — after a second `3`, report the malformed rows verbatim and stop. Completion condition: the merged list is on hand with every location appearing exactly once, or the run ended with 「無發現」 or with the reported error.
6. Report the finding list in Traditional Chinese, one line per merged row. **This skill never modifies code.** `jsc-hooks/hooks/write-guard.sh` in `review` mode enforces that in code: while this skill runs, it blocks write tools. Which tools those are is declared in that script's own header; never restate the list here, because a stale copy of it reads as coverage the guard does not have. Only claude has a `PreToolUse` hook, so codex, copilot, antigravity, and kiro never reach that guard — on those four this rule and the sub agent instructions are the only constraint. The caller decides what to fix.
Then release the review lock: run `jsc-hooks/hooks/write-guard.sh release`, which clears `$JSC_HOME/sessions/{sid}.lastskill`. That file is how the guard recognizes the running skill, and no event tells the guard a skill ended: leave it in place and the caller's first fix — the fix this very report asked for — is blocked by the audit that just finished. State the fallback in the report either way, because an older `jsc-hooks` treats `release` as an unknown mode and exits `0` without clearing anything: the lock then lifts by itself once the file is older than `JSC_WRITE_GUARD_TTL` (900 seconds by default), and `JSC_WRITE_GUARD=off` opens it immediately.
Completion condition: the report is handed to the caller, the release command has been run and the wait plus the `JSC_WRITE_GUARD=off` escape hatch are stated, and the fix decision is left to them.
7. Record how this run ended, as the very last thing this skill does — after the release, so a lock that would not clear is still visible to it:
`jsc-hooks/tools/report-status.sh skill-end jsc-review:api-doc {status} {exit} "{detail}"`
Resolve that path the way step 6 already resolves `jsc-hooks/hooks/write-guard.sh` — the sibling plugin directory, no separate lookup rule for this one call. **A missing script is not a failure here: skip this step in silence and let the run end as it stands.** The script swallows its own write errors and exits 0 even then, so nothing branches on its code either. This skill writes no code; it must not start failing over a line it could not write about itself.
| status | This skill's case |
| --- | --- |
| `ok` | Both aspects returned and the merged list reached the caller. A `merge-findings.sh` exit 1 is `ok` too: the audit ran and found nothing, which is a real answer and a different thing from never auditing |
| `blocked` | A precondition refused before any aspect ran: `swagger-detect.sh` exit 1 — the project has no Swagger package or no wiring, so the audit is skipped, which is exactly what step 2 reports and never a failure of this run. Exit 2 (project path missing, or no `grep` in the environment) and a `git diff` that refused belong here too |
| `degraded` | The finding list is reported but the close-out is short: `write-guard.sh release` did not clear `$JSC_HOME/sessions/{sid}.lastskill`, so the lock lingers until `JSC_WRITE_GUARD_TTL` and the caller's first fix is blocked by the audit that just finished |
| `failed` | The aspects ran and their findings never reached a report: `merge-findings.sh` exit 2, or a second exit 3 with malformed rows still on the table |
| `aborted` | There was nothing to audit — step 3's controller list came back empty, so a scoped run found no changed controller and no sub agent was spawned. Keep this apart from the `ok` above: both end in 「無發現」, and only the event tells a clean audit from an audit that never had a subject |
`{exit}` is the exit code of whatever decided the status, `0` for `ok`. `{detail}` is one short line well under 200 characters: controller and finding counts plus exit codes, never file paths, endpoint names, example payloads, or personal data.
Completion condition: the command has run, or the script was absent and this step was skipped.
## Severity
The 高、中、低 scale is the one in `references/smells.md`. Map this skill's findings onto it:
| Finding | Severity |
| --- | --- |
| A returnable status code has no declared response type | 高 |
| A declared response type does not match the body the code returns | 高 |
| An input or output has no description, whatever its type | 中 |
| A scalar input, output, or property has no example | 中 |
| An example sits on a data-model member while its own properties carry none | 中 |
| A data model property is missing from the recursive walk entirely | 中 |
| A placeholder example while real data was reachable | 低 |
| A derived example that is not marked as derived | 低 |
## Notes
- An example on a data-model member is a finding only when that model's properties carry none of their own. That is the example hung on the wrong layer: the caller holds one blob no property definition backs, and the layers below look answered while every one of them is empty. A model whose properties are all covered keeps any extra whole-object example it already has; this skill asks for no such example and reports none.
- Examples taken from a database must be de-identified. Never put personal data into a Swagger document.
- 「推導值」 is the literal marker for a derived example; keep it in the document text so the next reader knows the value was never observed.
- When there are no findings, report the literal 「無發現」 explicitly; never leave the report empty.