From d879e634c00e666f197b8fa068f411f379078880 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 31 Aug 2026 19:12:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontmatter-lint):=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?SKILL.md=20frontmatter=20=E8=A7=A3=E6=9E=90=E6=AA=A2=E6=9F=A5?= =?UTF-8?q?=E4=B8=A6=E4=BD=B5=E5=85=A5=E4=BE=8B=E8=A1=8C=E7=A8=BD=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What: - 新增 tools/lint-frontmatter.sh,掃一個 domain 每支 skills/*/SKILL.md 的 frontmatter,檢查分隔線成對、必要鍵齊全、未加引號的純量不含「冒號加空白」、起頭字元不是 YAML 特殊字元、加了引號的值收得起來,共五項。 - 改寫 skills/skill-check/SKILL.md 的第一組稽核,把這支腳本併進去成為第 2 步,原本的行為清單檢查、結束碼路由檢查、hook smoke 依序後移。 - 第二組留白的檢查清單項目由四項改成五項,第 3 步的合併說明、三組的完成條件、第 6 步的重驗完成條件同步改寫。 Why: - 抓到 6 支技能的 description 是未加引號的 YAML 純量、內容含「冒號加空白」。那在 YAML 是鍵的分隔符號,整份 frontmatter 當場語法錯誤。 - Antigravity 讀到語法錯誤就靜默丟棄整支技能。磁碟上 34 支,它只認 28 支。載入器不報、CLI 不報,技能清單只是少了幾列。 - 這種缺陷唯一的發現途徑是逐檔比對磁碟數量與載入數量。人工比對 10 個 domain 每次稽核都要重做一遍,還會漏。輸入輸出固定的判定就交給程式。 How: - 腳本用 awk 自己判定 YAML 1.2 的 plain scalar 規則,不相依 pyyaml。護欄不綁在一個不保證存在的相依上,才跑得到每一台機器。 - 單引號的跳脫是重複一次、雙引號的跳脫是反斜線,兩套規則不同,所以引號改用逐字掃描,不用正規表示式一次比對兩種。 - 結束碼分四種:0 是掃到 SKILL.md 且五項全過、1 是有不合格項目(清單走 stderr,格式 {檔案}:{鍵}:{說明})、2 是用法錯誤、3 是什麼都沒掃。 - SKILL.md 明寫退出 3 不算通過,並把「每個 domain 的 lint-frontmatter.sh 退出 0」列進第 6 步的完成條件。 Who: 屬 CLI hook 接線修正(jsc-hooks 0.3.4)在 meta 這一側的稽核工具。 --- skills/skill-check/SKILL.md | 21 ++--- tools/lint-frontmatter.sh | 168 ++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 10 deletions(-) create mode 100755 tools/lint-frontmatter.sh diff --git a/skills/skill-check/SKILL.md b/skills/skill-check/SKILL.md index 501b527..6b1da7c 100644 --- a/skills/skill-check/SKILL.md +++ b/skills/skill-check/SKILL.md @@ -1,6 +1,6 @@ --- name: skill-check -description: Routine compliance, script, hook, flow-efficiency, and cost-efficiency audit of the whole jsc skill set with no change request in hand. Sync every domain repo from the Gitea canonical marketplace, then run three parallel groups - lint-scripts.sh plus check-behaviors.sh plus hook smoke, the guidelines.md checklist audit, and a review of parallelism, tool extraction, repeated interaction, redundant checks, misplaced gates, and avoidable token, sub-agent, API, scan, or interaction cost. Confirm compliance fixes and optimization suggestions before applying them, re-check until accepted fixes pass, then open a PR per affected repo via jsc-git pr. Use for periodic or on-demand skill-set checks; not for applying a change request (use skillset-update) or editing one skill (use skill-update). +description: Routine compliance, script, hook, flow-efficiency, and cost-efficiency audit of the whole jsc skill set with no change request in hand. Sync every domain repo from the Gitea canonical marketplace, then run three parallel groups - lint-scripts.sh plus lint-frontmatter.sh plus check-behaviors.sh plus hook smoke, the guidelines.md checklist audit, and a review of parallelism, tool extraction, repeated interaction, redundant checks, misplaced gates, and avoidable token, sub-agent, API, scan, or interaction cost. Confirm compliance fixes and optimization suggestions before applying them, re-check until accepted fixes pass, then open a PR per affected repo via jsc-git pr. Use for periodic or on-demand skill-set checks; not for applying a change request (use skillset-update) or editing one skill (use skill-update). --- # skill-check — audit compliance, flow efficiency, and cost efficiency @@ -14,12 +14,13 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references The three review groups of step 2 all read this synced tree, so the sync finishes first. 2. Run the three review groups over the synced repos. They are independent — every one only reads, none writes a file — so **launch all three in parallel** and merge their results in step 3. - **Group 1 — validate scripts, behavior lists, and hooks.** + **Group 1 — validate scripts, frontmatter, behavior lists, and hooks.** 1. For every synced domain repo, run `tools/lint-scripts.sh {domain-path}`. One run per domain, and the runs go **in parallel** — no domain's verdict depends on another's. The tool covers three checks in one pass: `sh -n` syntax, executable bit, and an exit-code declaration in the file header. Route each exit code: 0 — the domain's scripts pass all three; 1 — the failing items are printed as `{file}:{check}:{detail}`, so report each one; 2 — usage error, the tool takes exactly one argument; 3 — nothing was scanned, because the path is missing or the domain has neither `tools/` nor `hooks/`. Record exit 3 as 「無腳本可掃」; a domain with no script directory is not a failure, but exit 3 is **never** a pass. - 2. For every synced domain repo, run `tools/check-behaviors.sh {domain-path}`. One run per domain, and the runs go **in parallel** alongside the `lint-scripts.sh` runs — no domain's verdict depends on another's. It compares `references/behaviors.md` against `skills/`: section per skill, dictionary order, one table per section, five rows, no empty content cell. Route each exit code: 0 — that domain's behavior list matches; 1 — the mismatches are printed on stderr as `{檔案}:{技能名}:{說明}`, so report every one as a compliance failure with the skill it belongs to; 2 — usage error, the tool takes exactly one argument; 3 — nothing was checked, because `references/behaviors.md` is missing, `skills/` is missing, or no `SKILL.md` was found. Record exit 3 as 「無清單可查」with the cause from stderr and carry it into the step 3 merge; a domain with no behavior list is a compliance failure, and exit 3 is **never** a pass. - 3. For every shell script directly named by a SKILL.md, confirm the skill routes every exit code the script's header declares. `lint-scripts.sh` proves the script exists and declares its codes; this check is the other half — that the caller branches on each of them. Report evidence as `skill file:line -> script path`. - 4. When the `jsc-hooks` domain is present, run `jsc-hooks/tools/wire-cli.sh smoke {cli}` for every CLI reported by `jsc-cli/tools/detect-clis.sh`; the per-CLI smokes run **in parallel**. When no CLI is detected, run `jsc-hooks/tools/wire-cli.sh smoke codex` as the minimum hook behavior check and label it 「預設 hook smoke」 in the report. Use `smoke`, not `purge` or rewiring actions, and set `JSC_READONLY=1` for the whole audit so a mistyped sub-command is refused in code (exit 6) instead of rewiring the machine; `status` and `smoke` are unaffected by that variable. Route each `smoke` exit code: 0 — the run passed its own assertions; 2 — usage error, so fix the CLI code and rerun; 4 — the smoke failed, which includes the script's own result-line count not matching what it expected. **Read the count from the script's `lines{數量}` output line; never write the number into this skill.** The script counts its own result lines and asserts them, so a hardcoded number here goes stale the moment a hook or a decision path is added — an out-of-date count in a SKILL.md is exactly what misled the previous audit. - 5. When a hook or script smoke fails, route it as a compliance failure with script name, exit code, output summary, and proposed fix. Do not continue to report the affected hook as compliant. + 2. For every synced domain repo, run `tools/lint-frontmatter.sh {domain-path}`. One run per domain, and the runs go **in parallel** alongside the `lint-scripts.sh` runs. It parses the frontmatter of every `skills/*/SKILL.md` without a YAML library — paired `---` delimiters, the required `name` and `description` keys, unquoted scalars carrying a colon-space or ending in a colon, unquoted scalars opening with `&`, `*`, `!`, `|`, `>`, `%`, `@` or a backtick, and quoted scalars that never close. Route each exit code: 0 — every SKILL.md in that domain parses; 1 — the failures are printed on stderr as `{檔案}:{鍵}:{說明}`, so report every one as a compliance failure with the file and key it belongs to; 2 — usage error, the tool takes exactly one argument; 3 — nothing was scanned, because the domain path or `skills/` is missing, or `skills/` holds no `SKILL.md`. Record exit 3 as 「無 frontmatter 可掃」with the cause from stderr and carry it into the step 3 merge; exit 3 is **never** a pass. This check exists because a broken frontmatter makes Antigravity drop the whole skill with **no error message at all** — 34 skills on disk loaded as 28, and only a file-by-file comparison found it. + 3. For every synced domain repo, run `tools/check-behaviors.sh {domain-path}`. One run per domain, and the runs go **in parallel** alongside the `lint-scripts.sh` runs — no domain's verdict depends on another's. It compares `references/behaviors.md` against `skills/`: section per skill, dictionary order, one table per section, five rows, no empty content cell. Route each exit code: 0 — that domain's behavior list matches; 1 — the mismatches are printed on stderr as `{檔案}:{技能名}:{說明}`, so report every one as a compliance failure with the skill it belongs to; 2 — usage error, the tool takes exactly one argument; 3 — nothing was checked, because `references/behaviors.md` is missing, `skills/` is missing, or no `SKILL.md` was found. Record exit 3 as 「無清單可查」with the cause from stderr and carry it into the step 3 merge; a domain with no behavior list is a compliance failure, and exit 3 is **never** a pass. + 4. For every shell script directly named by a SKILL.md, confirm the skill routes every exit code the script's header declares. `lint-scripts.sh` proves the script exists and declares its codes; this check is the other half — that the caller branches on each of them. Report evidence as `skill file:line -> script path`. + 5. When the `jsc-hooks` domain is present, run `jsc-hooks/tools/wire-cli.sh smoke {cli}` for every CLI reported by `jsc-cli/tools/detect-clis.sh`; the per-CLI smokes run **in parallel**. When no CLI is detected, run `jsc-hooks/tools/wire-cli.sh smoke codex` as the minimum hook behavior check and label it 「預設 hook smoke」 in the report. Use `smoke`, not `purge` or rewiring actions, and set `JSC_READONLY=1` for the whole audit so a mistyped sub-command is refused in code (exit 6) instead of rewiring the machine; `status` and `smoke` are unaffected by that variable. Route each `smoke` exit code: 0 — the run passed its own assertions; 2 — usage error, so fix the CLI code and rerun; 4 — the smoke failed, which includes the script's own result-line count not matching what it expected. **Read the count from the script's `lines{數量}` output line; never write the number into this skill.** The script counts its own result lines and asserts them, so a hardcoded number here goes stale the moment a hook or a decision path is added — an out-of-date count in a SKILL.md is exactly what misled the previous audit. + 6. When a hook or script smoke fails, route it as a compliance failure with script name, exit code, output summary, and proposed fix. Do not continue to report the affected hook as compliant. **Group 2 — audit every skill of every domain against the guidelines.md audit checklist.** This group MUST run as a sub agent, one sub agent per domain repo, and those sub agents run **in parallel**. Each sub agent reports its findings: skill, failed checklist item, evidence (file:line), proposed fix. Cover the checklist's four flow checks by name, not only the naming and language items: - Every step number, file path and section title the skill references — inside itself and in other files — really exists (the pointer points at something). @@ -27,7 +28,7 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references - Every external call (script, API, other skill) states what to do on failure and routes every exit code. - No gate the skill installs blocks the only path that lifts that gate. - Four checklist items are **already decided by group 1** and must not be re-run here: `sh -n` on every `tools/` and `hooks/` script, script existence with the executable bit, the hook smoke, and the `references/behaviors.md` match. Tell each sub agent to skip those four and leave them blank; the main agent fills them in from the group 1 verdicts when merging in step 3. Re-scanning the same files in every domain sub agent buys nothing — group 1 already scanned them all, with the same tools, on the same synced tree. + Five checklist items are **already decided by group 1** and must not be re-run here: `sh -n` on every `tools/` and `hooks/` script, script existence with the executable bit, the hook smoke, the `references/behaviors.md` match, and the `lint-frontmatter.sh` verdict. Tell each sub agent to skip those five and leave them blank; the main agent fills them in from the group 1 verdicts when merging in step 3. Re-scanning the same files in every domain sub agent buys nothing — group 1 already scanned them all, with the same tools, on the same synced tree. **Group 3 — a flow and cost optimization review**, kept separate from the compliance audit. Each aspect **MUST run as a sub agent**, and the six aspects run in parallel with each other and with groups 1 and 2: @@ -42,8 +43,8 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references Each optimization finding reports skill, aspect, evidence (file:line), current flow step count, proposed flow step count, what time or interaction it saves, what cost it saves, current cost driver, proposed cost driver, whether correctness decreases, and which protection would be weakened if any. Cost savings may be token volume, sub-agent count, API calls, file scans, full-repo audits, or user prompts. Keep optimization findings separate from compliance failures. - Completion condition for all three groups: every domain has a `lint-scripts.sh` verdict and a `check-behaviors.sh` verdict, every script named by a SKILL.md has an exit-code-routing verdict, and every smoked CLI has a `smoke` exit code plus the `lines` value the script printed for it; every domain has a group 2 audit result that names a verdict for all checklist items — the four flow checks included, and the four group 1 items left blank for the step 3 merge rather than re-scanned; and every one of the six aspects has returned a verdict for every domain, 「無發現」 where an aspect found nothing. -3. Merge the three groups, then present compliance failures and optimization findings separately via the `jsc-ask:ask` decision tree. Merging means one thing in code: fill the four skipped checklist items of every group 2 sub agent report from the matching group 1 verdicts, so each domain ends with one complete checklist and no item counted twice. + Completion condition for all three groups: every domain has a `lint-scripts.sh` verdict, a `lint-frontmatter.sh` verdict and a `check-behaviors.sh` verdict, every script named by a SKILL.md has an exit-code-routing verdict, and every smoked CLI has a `smoke` exit code plus the `lines` value the script printed for it; every domain has a group 2 audit result that names a verdict for all checklist items — the four flow checks included, and the five group 1 items left blank for the step 3 merge rather than re-scanned; and every one of the six aspects has returned a verdict for every domain, 「無發現」 where an aspect found nothing. +3. Merge the three groups, then present compliance failures and optimization findings separately via the `jsc-ask:ask` decision tree. Merging means one thing in code: fill the five skipped checklist items of every group 2 sub agent report from the matching group 1 verdicts, so each domain ends with one complete checklist and no item counted twice. - Compliance failure options: apply the proposed fix / skip / custom fix. Every option states its impact scope, for example skipping leaves the skill non-compliant until the next audit. - Optimization options: apply / defer / custom. Any suggestion that weakens a protection must name the protection it removes and must not be applied unless the user explicitly accepts that tradeoff. Cost optimization may move, merge, cache, or narrow checks; it must not delete a compliance check only because it is expensive. @@ -56,5 +57,5 @@ Single source of guidelines: [`../../references/guidelines.md`](../../references - Exit 0 — every copy holds identical bytes; the script verifies that itself. Completion condition: the script exits 0 and prints the touched paths. -6. Re-run the group 1 script, behavior-list, and hook validation, re-check the guidelines.md audit checklist for every touched skill, then re-run the optimization aspect that produced each accepted optimization. These three re-runs are as independent as the first pass, so run them **in parallel** and merge them the same way step 3 did. On any compliance failure, **return to step 3**: confirm and fix again, until all accepted compliance fixes pass. On an accepted optimization that does not produce the promised step reduction or cost reduction, or still weakens correctness beyond the recorded decision, return to step 3 for a new decision. Completion condition: `tools/lint-scripts.sh` exits 0 or 3 for every domain, `tools/check-behaviors.sh` exits 0 for every domain, every hook smoke exits 0 with the `lines` count the script itself asserted, all checklist items pass, and every accepted optimization has a matching verification result. +6. Re-run the group 1 script, frontmatter, behavior-list, and hook validation, re-check the guidelines.md audit checklist for every touched skill, then re-run the optimization aspect that produced each accepted optimization. These three re-runs are as independent as the first pass, so run them **in parallel** and merge them the same way step 3 did. On any compliance failure, **return to step 3**: confirm and fix again, until all accepted compliance fixes pass. On an accepted optimization that does not produce the promised step reduction or cost reduction, or still weakens correctness beyond the recorded decision, return to step 3 for a new decision. Completion condition: `tools/lint-scripts.sh` exits 0 or 3 for every domain, `tools/lint-frontmatter.sh` exits 0 for every domain — exit 3 is 「什麼都沒掃」 and never counts as a pass — `tools/check-behaviors.sh` exits 0 for every domain, every hook smoke exits 0 with the `lines` count the script itself asserted, all checklist items pass, and every accepted optimization has a matching verification result. 7. Call `jsc-git:pr` once per affected domain repo to open a Push Request. Completion condition: every affected repo has a PR URL, and all URLs are reported in one table with the format in [`../../references/pr-report.md`](../../references/pr-report.md). diff --git a/tools/lint-frontmatter.sh b/tools/lint-frontmatter.sh new file mode 100755 index 0000000..d977b47 --- /dev/null +++ b/tools/lint-frontmatter.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env sh +# lint-frontmatter.sh — 檢查一個 domain 存取庫每支 skills/*/SKILL.md 的 frontmatter 能不能安全解析。 +# +# 用法: lint-frontmatter.sh +# +# 檢查五項(掃 {domain-path}/skills/*/SKILL.md): +# 1. 分隔線 — 第一行是「---」,而且找得到成對的收尾「---」。缺一邊,底下整段都不是 +# frontmatter,鍵一個都讀不到。 +# 2. 必要鍵 — 「name」與「description」都在,而且值不是空的。載入器靠這兩個鍵認技能。 +# 3. 冒號 — 未加引號的純量不得含「冒號加空白」,也不得以冒號結尾。那在 YAML 是鍵的 +# 分隔符號,解析器會把一行拆成兩個鍵,整份 frontmatter 當場語法錯誤。 +# 4. 起頭字元 — 未加引號的純量不得以 & * ! | > % @ ` 起頭。這八個在 YAML 1.2 分別是錨點、 +# 別名、標籤、區塊純量、指令與保留字元,起頭寫了就不是原本那串字。 +# 5. 引號 — 加了引號的值要收得起來:單引號內部的「'」要寫成「''」,收尾引號之後除了 +# 註解不得有殘餘。修這個缺陷的手法就是補單引號,補歪了照樣是語法錯誤。 +# +# 為什麼要這支: 2026-08-31 抓到 6 支技能的 description 是未加引號的純量、內容含「冒號加空白」。 +# 那在 YAML 是語法錯誤,Antigravity 讀到就**靜默丟棄整支技能**——磁碟上 34 支,它只認 28 支, +# 而且**完全沒有錯誤訊息**:載入器不報、CLI 不報、技能清單只是少了幾列。這種缺陷唯一的發現 +# 途徑是逐檔比對磁碟數量與載入數量,人工比對 10 個 domain 每次稽核都要重做一遍,還會漏。 +# 輸入輸出固定的判定就交給程式,別靠眼睛。 +# +# 為什麼不用 YAML 套件: 本機沒有 pyyaml,而護欄不該把自己綁在一個不保證存在的相依上。這五項 +# 都只需要 YAML 1.2 的 plain scalar 規則,自己判定就夠,也才跑得到每一台機器上。 +# +# 輸出: 一行一個不合格項目,格式 {檔案}:{鍵}:{說明}(stderr);通過時在 stderr 印一行摘要。 +# 結構性問題(分隔線、必要鍵、無法辨識的一行)的「鍵」欄寫 frontmatter。stdout 不印東西。 +# 結束碼: 0=掃到 SKILL.md 且五項全過 +# 1=有不合格項目(清單在 stderr) +# 2=用法錯誤(本腳本只吃一個參數) +# 3=domain 路徑不存在、找不到 {domain-path}/skills/,或 skills/ 底下一支 SKILL.md +# 都沒有——**什麼都沒掃**,不等於通過 +set -u + +usage() { + echo 'usage: lint-frontmatter.sh ' >&2 + exit 2 +} + +[ "$#" -eq 1 ] || usage +DOMAIN=${1%/} +[ -n "$DOMAIN" ] || usage + +SKILLS="$DOMAIN/skills" +[ -d "$DOMAIN" ] || { echo "找不到 domain 路徑:$DOMAIN" >&2; exit 3; } +[ -d "$SKILLS" ] || { echo "找不到 skills/:$SKILLS" >&2; exit 3; } + +TMP=$(mktemp) || { echo "無法建立暫存檔" >&2; exit 3; } +trap 'rm -f "$TMP"' EXIT + +find "$SKILLS" -mindepth 2 -maxdepth 2 -type f -name 'SKILL.md' 2>/dev/null \ + | LC_ALL=C sort > "$TMP" +[ -s "$TMP" ] || { echo "skills/ 底下沒有任何 SKILL.md,無 frontmatter 可掃:$SKILLS" >&2; exit 3; } + +BOM=$(printf '\357\273\277') + +hit=0 +total=0 +while IFS= read -r f; do + [ -n "$f" ] || continue + total=$((total + 1)) + awk -v f="$f" -v bom="$BOM" ' +function trim(s) { gsub(/^[ \t\r]+/, "", s); gsub(/[ \t\r]+$/, "", s); return s } +function rep(k, m) { printf "%s:%s:%s\n", f, k, m; bad = 1 } + +# 掃過一段引號括起來的值,回傳收尾引號之後的殘餘;收不起來就回哨兵值。 +# 為什麼要自己逐字掃: 單引號的跳脫是「重複一次」、雙引號的跳脫是反斜線,兩套規則不同, +# 用正規表示式一次比對兩種只會在其中一種上判錯。 +function scan_quoted(v, q, i, c, n) { + n = length(v) + i = 2 + while (i <= n) { + c = substr(v, i, 1) + if (q == "\"" && c == "\\") { i += 2; continue } + if (c == q) { + if (q == "\047" && substr(v, i + 1, 1) == "\047") { i += 2; continue } + return substr(v, i + 1) + } + i++ + } + return "\001" +} + +BEGIN { state = 0; bad = 0 } +{ sub(/\r$/, "") } + +NR == 1 { + line = $0 + sub("^" bom, "", line) + if (trim(line) != "---") { + rep("frontmatter", "第一行不是 ---,整份 frontmatter 讀不到,載入器會靜默丟棄這支技能") + state = 3 # 已判定並回報,END 不必再補話 + exit + } + state = 1 + next +} + +# frontmatter 收尾。YAML 的文件結束標記「...」一樣算收尾。 +state == 1 && (trim($0) == "---" || trim($0) == "...") { state = 2; next } + +state == 1 { + if ($0 ~ /^[ \t]/) next # 縮排的續行或巢狀對應,判不出就不判 + if ($0 ~ /^[ \t]*$/) next # 空行 + if ($0 ~ /^#/) next # 註解 + if ($0 ~ /^- /) next # 與鍵同縮排的序列項 + + if ($0 !~ /^[A-Za-z0-9_.-]+:([ \t]|$)/) { + rep("frontmatter", "這一行既不是鍵也不是續行,YAML 解析會在這裡中斷:" substr($0, 1, 40)) + next + } + + ci = index($0, ":") + k = substr($0, 1, ci - 1) + v = trim(substr($0, ci + 1)) + + if (k in seen) rep(k, "同一個鍵出現兩次,後面那份會靜默蓋掉前面那份") + seen[k] = 1 + val[k] = v + + if (v == "") next # 值在下一段,前面的縮排規則已經放過 + + q = substr(v, 1, 1) + if (q == "\047" || q == "\"") { + rest = scan_quoted(v, q) + if (rest == "\001") { + if (q == "\047") rep(k, "單引號沒有收尾,內部的 \047 要寫成 \047\047") + else rep(k, "雙引號沒有收尾") + } else { + rest = trim(rest) + if (rest != "" && substr(rest, 1, 1) != "#") + rep(k, "收尾引號之後還有內容,解析器會當成語法錯誤:" substr(rest, 1, 30)) + } + next + } + + # 以下都是未加引號的純量(plain scalar)。 + if (index("&*!|>%@`", q) > 0) + rep(k, "未加引號的純量以 YAML 特殊字元「" q "」起頭,會被當成錨點、別名、標籤、區塊純量或指令") + + if (index(v, ": ") > 0) + rep(k, "未加引號的純量含「冒號加空白」,YAML 會把它當成鍵的分隔符號,整份 frontmatter 語法錯誤;整串加單引號即可") + else if (substr(v, length(v), 1) == ":") + rep(k, "未加引號的純量以冒號結尾,YAML 會把它當成鍵的分隔符號;整串加單引號即可") +} + +END { + if (state == 0) { + rep("frontmatter", "檔案是空的,沒有 frontmatter") + } else if (state == 1) { + rep("frontmatter", "frontmatter 分隔線不成對,找不到收尾的 ---") + } else if (state == 2) { + if (!("name" in seen)) rep("frontmatter", "缺必要鍵 name") + else if (val["name"] == "") rep("name", "必要鍵的值是空的") + if (!("description" in seen)) rep("frontmatter", "缺必要鍵 description") + else if (val["description"] == "") rep("description", "必要鍵的值是空的") + } + exit bad +} +' "$f" >&2 || hit=1 +done < "$TMP" + +if [ "$hit" -eq 0 ]; then + echo "frontmatter 檢查通過:$total 支(分隔線、必要鍵、冒號、起頭字元、引號)" >&2 +else + echo "frontmatter 檢查有不合格項目:共掃 $total 支,清單在上面" >&2 +fi +exit $hit