feat(api-doc): 範例只掛純量成員,類別型往下遞迴

- 使用者要求改判準。類別型參數與中間層的類別屬性只留說明,範例責任往下推給屬性,一路走到最內層的純量。
- 一個值只有一個出處。範例掛在父層,屬性一改就過期,讀的人拿到的是沒有屬性定義背書的一份資料。
- 集合看元素型別,不看外殼;字典看值型別。字串集合與字串判出來一樣,位址集合與位址判出來也一樣。
- 壞味道清單原本寫「輸入與輸出參數都必須有範例」,與新判準衝突,兩支技能會對同一段程式碼給出不同標準。一併改成同一套。
- 說明與範例的兩份檢核表併成一個 sub agent,同一批資料模型檔只讀一次。
- 順帶補上偵測腳本結束碼二漏掉的一個原因:環境缺 grep。原本只寫參數與路徑,遇到這個原因換路徑重跑永遠清不掉。
This commit is contained in:
2026-08-31 11:07:05 +08:00
parent a0bd488ac8
commit f6cd1f9ef7
2 changed files with 39 additions and 17 deletions
+6 -5
View File
@@ -140,14 +140,15 @@
### 5.4 輸入與輸出範例
- **定義**:輸入與輸出參數都必須有範例;範例內容**優先嘗試從資料庫取得真實資料,失敗才透過邏輯推理**產生。
- **偵測訊號**:註解缺範例;範例與型別不符;範例顯然是佔位假資料而環境可取得真實資料。
- **建議重構手法**:以可用的連線查詢一筆代表性資料當範例(去識別化,不可含個資);無法連線才以邏輯推理造出合理範例並標明為推理值。
- **定義**:輸入與輸出參數都必須有說明,範例則只掛在**純量**成員上。純量參數與純量屬性(字串、數值、布林、日期、列舉)要有範例。類別型參數與中間層的類別屬性只要說明,自己不掛範例,範例責任往下推給該類別的屬性。集合看元素型別判斷:元素是純量就比照純量,附一份列出幾個元素的範例;元素是類別就比照類別,只留說明,往下走進元素型別。字典看值型別,判準相同。範例內容**優先嘗試從資料庫取得真實資料,失敗才透過邏輯推理**產生。
- **偵測訊號**:純量參數或純量屬性缺範例;範例與型別不符;範例顯然是佔位假資料而環境可取得真實資料;範例掛在類別型成員上,該類別的屬性卻一個範例都沒有。
- **建議重構手法**:以可用的連線查詢一筆代表性資料當範例(去識別化,不可含個資);無法連線才以邏輯推理造出合理範例並標明為推理值;範例掛錯層就往下搬到該類別的每個純量屬性上。
- **注意**:型別判準與 `jsc-review:api-doc` 的檢核表 A 完全相同,同一個成員在兩邊判出來的答案一樣。分工不變:本項只看原始碼註解,Swagger 文件屬性歸 `api-doc`,同一個標的不重複回報。
### 5.5 巢狀結構註解
- **定義**:如果參數有巢狀結構(例如 class 內還有 class),就必須完全補齊每一層的註解。
- **偵測訊號**:DTO/ViewModel 僅頂層有註解;內層類別、集合元素型別的欄位無說明或無範例。
- **定義**:如果參數有巢狀結構(例如 class 內還有 class),就必須完全補齊每一層的註解。每一層的純量屬性要有說明與範例,中間層的類別屬性只要說明;集合往元素型別走,遞迴走到「屬性全是純量」那一層為止。
- **偵測訊號**:DTO/ViewModel 僅頂層有註解;內層類別、集合元素型別的欄位無說明;任一層的純量屬性缺範例;遞迴半途停住,某一層的屬性完全沒被走到。
- **建議重構手法**:逐層補齊 5.1–5.4;巢狀過深(≥ 3 層)時同時評估 Extract Class 是否被濫用。
### 5.6 內含功能的導向連結
+33 -12
View File
@@ -1,6 +1,6 @@
---
name: api-doc
description: Audit an API project's Swagger/OpenAPI documentation: every returnable HTTP status code declares a response type, and every input and output carries a description plus a real data example, recursively down nested data models. 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.
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
@@ -21,22 +21,40 @@ Audit whether an API project's Swagger (OpenAPI) documentation is complete enoug
## 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` bad argument or missing path. 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 error message verbatim and stop; the caller supplies a valid project path and re-runs. Completion condition: the run has ended with an honest reason, 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. Completion condition: the controller list is non-empty and reported; an empty list ends the run with the literal 「無發現」.
4. Audit in three aspects, and every aspect **MUST run as a sub agent**; the three may run in parallel:
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 Parameter description and example | Every input parameter and every output field has a description and an example. Examples come from real project data first — query the project's database, seed data, or fixtures. Only when real data is unreachable, derive an example by logic and mark it as a derived value in the document itself |
| 3 Recursive data model | When a parameter or return value is a data model, aspect 2 applies to every one of its properties. A model containing another model recurses to the innermost layer. Walk in from the controller and follow the input and output types down |
| 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 |
Instructions for each sub agent: read only, change nothing; report each finding as `file:line`, the aspect, severity, one sentence of evidence, and the concrete fix (which attribute to add, on which member). Findings are reported in Traditional Chinese.
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.
Completion condition: all three aspects have returned — an aspect with nothing to report still returns 「無發現」 for itself.
5. Merge the three aspects' findings: deduplicate by location, then sort by severity. Start this step only once all three have returned. Completion condition: every finding appears exactly once, ordered 高 → 中 → 低.
6. Report the finding list. **This skill never modifies code**; the caller decides what to fix. Completion condition: the report is handed to the caller and the fix decision is left to them.
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.
## Severity
@@ -46,13 +64,16 @@ The 高、中、低 scale is the one in `references/smells.md`. Map this skill's
| --- | --- |
| 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 | 中 |
| 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.