@ccoalm/ccl-skills 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-data-acquisition.md +3 -1
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-disclosure-channels.md +2 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +4 -4
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +8 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +12 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +18 -0
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +12 -1
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +6 -0
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +85 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +123 -0
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +4 -4
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/deliverable-doc-genre-skeletons.md +133 -0
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/doc-charter-first.md +2 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/figure-and-table-craft.md +318 -0
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/AGENTS.md +46 -0
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/doc-lint.py +246 -0
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/figure-lint.py +1092 -0
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/mutation_probe.sh +100 -0
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/test_figure_and_doc_lint.sh +375 -0
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/control.md +10 -0
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/empty-header.md +6 -0
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fake-header.md +13 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fenced-noise.md +14 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-dangling.md +5 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan-captioned.md +11 -0
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan.md +9 -0
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig.png +0 -0
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/imbalance.md +41 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/no-unit.md +8 -0
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/should-be-chart.md +11 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/tables-only-clean.md +35 -0
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/unfilled.md +7 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/wide-table.md +5 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/bad-viewbox.svg +9 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/blackmarker.svg +9 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/control.svg +12 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/crossings.svg +12 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/cvd-confusable.svg +9 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/decorative-line.svg +10 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-no-arrow.svg +11 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-vague.svg +12 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-contract.json +21 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-is-a-list.svg +12 -0
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/flow-mixed.svg +13 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/low-contrast.svg +12 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/malformed.svg +1 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-aria.svg +9 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-group.svg +10 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-legend.svg +9 -0
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-title.svg +12 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-viewbox.svg +9 -0
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/offcontract-shape.svg +13 -0
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/overflow.svg +13 -0
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/transformed.svg +9 -0
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/ungrouped-card.svg +12 -0
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/unlabeled-edge.svg +13 -0
  58. package/dist/assets/release.json +244 -14
  59. package/package.json +1 -1
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ # 差分敏感度实测:逐谓词把其 code 字面量替换掉使其在结果中消失,
3
+ # 确认自测套件转红。绿的谓词 = 该谓词没有覆盖。
4
+ #
5
+ # 为什么必须"施加"而不是"断言":本目录实测踩过两种无效突变——
6
+ # ① 改的是 add() 的 severity 参数,而断言比较 code 集合,
7
+ # 改了 oracle 不观测的维度,全绿是假通过;
8
+ # ② 脚本崩溃时也没有 FAIL 行,只看 FAIL 行会把崩溃读成绿。
9
+ # 所以判据同时看退出码与 FAIL 行。
10
+ set -uo pipefail
11
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
12
+ work="$(mktemp -d)"; trap 'rm -rf "${work}"' EXIT
13
+ cp -R "${here}" "${work}/scripts"
14
+ cd "${work}/scripts" || exit 1
15
+
16
+ bash test_figure_and_doc_lint.sh >/dev/null 2>&1 || {
17
+ echo "BASELINE-RED: 未突变时套件就是红的,实验无效"; exit 1; }
18
+ echo "baseline: GREEN"
19
+
20
+ # 谓词清单从**源码派生**,不手写:手写清单会漏掉新增谓词,
21
+ # 而漏掉的那条就没有任何覆盖。独立评审实测抓到过——手写的 24 条里
22
+ # 漏了 PARSE / C4-VIEWBOX / C4-EDGE-VAGUE 三条。
23
+ extract_codes() { # <linter-file>
24
+ # 必须覆盖**所有发射路径与所有档位**。踩过两次分母偏小:
25
+ # ① 早先只认 add(...) 与错误元组,漏掉走 findings.append({'code': ...}) 的
26
+ # CONTRACT-MISSING / CONTRACT-INVALID;
27
+ # ② 档位只写了 ERROR|WARN,于是把一条谓词改成 INFO 就让它**静默退出分母**,
28
+ # 总数看起来不变、覆盖却少了一条。档位一律用 [A-Z]+ 匹配,不枚举。
29
+ python3 - "$1" <<'PYEOF'
30
+ import re, sys
31
+ src = open(sys.argv[1], encoding="utf8").read()
32
+ pats = [
33
+ r"""add\(\s*['"][A-Z]+['"]\s*,\s*['"]([A-Z0-9][A-Z0-9-]*)['"]""", # add('<档位>', 'CODE'
34
+ r"""\(\s*['"][A-Z]+['"]\s*,\s*['"]([A-Z0-9][A-Z0-9-]*)['"]""", # ('<档位>', 'CODE'
35
+ r"""['"]code['"]\s*:\s*['"]([A-Z0-9][A-Z0-9-]*)['"]""", # {'code': 'CODE'
36
+ ]
37
+ codes = set()
38
+ for pat in pats:
39
+ codes |= set(re.findall(pat, src))
40
+ codes.discard("MUTANT-GONE")
41
+ for c in sorted(codes):
42
+ print(c)
43
+ PYEOF
44
+ }
45
+ codes_fig="$(extract_codes figure-lint.py)"
46
+ codes_doc="$(extract_codes doc-lint.py)"
47
+ echo "codes_from_source: figure=$(echo "${codes_fig}" | wc -w | tr -d ' ') doc=$(echo "${codes_doc}" | wc -w | tr -d ' ')"
48
+
49
+ pass=0; total=0; missed=""
50
+ probe() {
51
+ local file="$1" code="$2"
52
+ total=$((total + 1))
53
+ cp "${file}" "${file}.bak"
54
+ MUT_FILE="${file}" MUT_CODE="${code}" python3 -c '
55
+ import os
56
+ p = os.environ["MUT_FILE"]
57
+ s = open(p, encoding="utf8").read()
58
+ open(p, "w").write(s.replace("'"'"'" + os.environ["MUT_CODE"] + "'"'"'", "'"'"'MUTANT-GONE'"'"'"))
59
+ '
60
+ local out rc
61
+ out="$(bash test_figure_and_doc_lint.sh 2>&1)"; rc=$?
62
+ mv "${file}.bak" "${file}"
63
+ if [ "${rc}" -ne 0 ] && printf '%s' "${out}" | grep -q FAIL; then
64
+ pass=$((pass + 1)); printf ' %-28s RED ok\n' "${code}"
65
+ else
66
+ missed="${missed} ${code}"; printf ' %-28s GREEN NO-COVERAGE\n' "${code}"
67
+ fi
68
+ }
69
+ for c in ${codes_fig}; do probe figure-lint.py "${c}"; done
70
+ for c in ${codes_doc}; do probe doc-lint.py "${c}"; done
71
+ # 派生集合必须覆盖 linter 在自测语料上实际输出过的每一个 code。
72
+ # 不断言的话,一条新增谓词只要用了未被派生正则识别的发射路径,就会静默无覆盖。
73
+ emitted="$(
74
+ { python3 figure-lint.py tests/svg/*.svg --json 2>/dev/null || true
75
+ python3 doc-lint.py tests/doc/*.md --json 2>/dev/null || true
76
+ } | python3 -c '
77
+ import json, sys
78
+ codes = set()
79
+ for chunk in sys.stdin.read().split("\n{"):
80
+ text = chunk if chunk.startswith("{") else "{" + chunk
81
+ try:
82
+ d = json.loads(text)
83
+ except Exception:
84
+ continue
85
+ for v in d.get("files", d).values(): # doc-lint 的 JSON 顶层就是文件映射,没有 files 键
86
+ codes |= {x["code"] for x in v["findings"]}
87
+ print(" ".join(sorted(codes)))
88
+ ')"
89
+ # codes_* 是换行分隔的,必须先规范成单空格分隔再做包含匹配,
90
+ # 否则 case 模式永远匹配不上、把每一条都误报成缺口(本脚本实测踩过)。
91
+ derived=" $(printf '%s %s' "${codes_fig}" "${codes_doc}" | tr '\n' ' ' | tr -s ' ') "
92
+ for c in ${emitted}; do
93
+ case "${derived}" in
94
+ *" ${c} "*) ;;
95
+ *) echo "DERIVATION-GAP: ${c} 实际会被输出,却不在派生清单里——该谓词无突变覆盖"; missed="${missed} ${c}";;
96
+ esac
97
+ done
98
+ echo "differential_sensitivity=${pass}/${total}"
99
+ [ -n "${missed}" ] && { echo "uncovered:${missed}"; exit 1; }
100
+ echo "mutation_probe_ok"
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env bash
2
+ # 逐谓词差分自测:每个 fixture 只应触发它自己的那一条,控制组必须干净。
3
+ #
4
+ # 这是 references/figure-and-table-craft.md §8 第 1、2 条的机械化:
5
+ # 1) 先证明检查器能报失败再信它的绿;
6
+ # 2) 改判据必须同步改 fixture——本测试就是"fixture 失效但仍通过"的防线。
7
+ #
8
+ # 已踩过的坑,改本文件前先读:
9
+ # - CJK 标点紧跟 $var 会被 bash 吞进变量名(踩过两次)——插值一律用 ${var}。
10
+ # - 改函数签名必须同步改调用点:曾把 ext 参数加进签名却漏改调用点,
11
+ # 第一条期望(控制组)被当成 ext 吃掉,control.svg 变脏仍全绿。
12
+ # 现在参数不足会直接判红,且期望表是**全量对应**:报告里多一个或少一个文件都判红,
13
+ # 所以意外文件(如通配到 figure-contract.json)不会再被无声忽略。
14
+ set -uo pipefail
15
+
16
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
17
+ fail=0
18
+
19
+ note() { printf '%s\n' "$*"; }
20
+ bad() { printf 'FAIL %s\n' "$*"; fail=1; }
21
+
22
+ # run_expect <linter> <fixture-dir> <ext> <expectation>...
23
+ # expectation 形如 "file=CODE[,CODE]";等号后为空表示该 fixture 必须干净。
24
+ run_expect() {
25
+ if [ "$#" -lt 4 ]; then
26
+ bad "run_expect 参数不足(需 linter dir ext expectation...),实得 $# 个"; return
27
+ fi
28
+ local linter="$1" dir="$2" ext="$3"; shift 3
29
+ local json
30
+ # 两个 linter 在有发现时都返回非 0(json 模式亦然),所以不能用退出码判执行失败;
31
+ # 判据是"输出能否解析成 JSON"。
32
+ json="$(python3 "${here}/${linter}" "${dir}"/*."${ext}" --json 2>/dev/null)"
33
+ printf '%s' "${json}" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null || {
34
+ bad "${linter} 未产出可解析 JSON(${dir})"; return; }
35
+
36
+ local result
37
+ result="$(printf '%s' "${json}" | LINT_EXPECT="$*" python3 -c '
38
+ import json, sys, os
39
+ d = json.load(sys.stdin)
40
+ files = d.get("files", d)
41
+ actual = {os.path.basename(k): sorted({x["code"] for x in v["findings"]}) for k, v in files.items()}
42
+ want = {}
43
+ for spec in os.environ["LINT_EXPECT"].split():
44
+ name, _, codes = spec.partition("=")
45
+ want[name] = sorted(c for c in codes.split(",") if c)
46
+ problems = []
47
+ for name in sorted(set(want) - set(actual)):
48
+ problems.append("期望表列了但报告里没有: " + name)
49
+ for name in sorted(set(actual) - set(want)):
50
+ problems.append("报告里出现但期望表未列(发现=%s): %s" % (actual[name] or [], name))
51
+ for name in sorted(set(want) & set(actual)):
52
+ if want[name] != actual[name]:
53
+ problems.append("%s 期望 %s 实得 %s" % (name, want[name] or [], actual[name] or []))
54
+ print("\n".join(problems))
55
+ ')" || { bad "${linter} 结果比对失败"; return; }
56
+ if [ -n "${result}" ]; then
57
+ while IFS= read -r line; do
58
+ [ -n "${line}" ] && bad "${linter}: ${line}"
59
+ done <<<"${result}"
60
+ fi
61
+ }
62
+
63
+ note "== figure-lint 逐谓词差分 =="
64
+ run_expect figure-lint.py "${here}/tests/svg" svg \
65
+ 'control.svg=' \
66
+ 'no-title.svg=C4-TITLE' \
67
+ 'no-legend.svg=C4-LEGEND' \
68
+ 'low-contrast.svg=WCAG-143' \
69
+ 'overflow.svg=SVG-OVERFLOW' \
70
+ 'no-group.svg=GROUPING' \
71
+ 'ungrouped-card.svg=GROUPING' \
72
+ 'unlabeled-edge.svg=C4-EDGE-LABEL' \
73
+ 'offcontract-shape.svg=CONTRACT-COLOR-TOKEN' \
74
+ 'malformed.svg=PARSE' \
75
+ 'bad-viewbox.svg=GEOMETRY' \
76
+ 'no-viewbox.svg=C4-VIEWBOX' \
77
+ 'edge-no-arrow.svg=C4-EDGE-DIRECTION' \
78
+ 'edge-vague.svg=C4-EDGE-VAGUE' \
79
+ 'transformed.svg=CONTRAST-UNSUPPORTED' \
80
+ 'crossings.svg=GRAPH-CROSSINGS' \
81
+ 'figure-is-a-list.svg=FIGURE-IS-A-LIST,GROUPING' \
82
+ 'flow-mixed.svg=FLOW-DIRECTION-MIXED' \
83
+ 'no-aria.svg=FIGURE-A11Y-STRUCTURE' \
84
+ 'cvd-confusable.svg=' \
85
+ 'decorative-line.svg=' \
86
+ 'blackmarker.svg=CONTRACT-COLOR-TOKEN'
87
+
88
+ note "== doc-lint 逐谓词差分 =="
89
+ run_expect doc-lint.py "${here}/tests/doc" md \
90
+ 'control.md=' \
91
+ 'tables-only-clean.md=' \
92
+ 'fake-header.md=WCAG-131-FAKE-HEADING' \
93
+ 'no-unit.md=TABLE-NO-UNIT' \
94
+ 'should-be-chart.md=ICD203-9-SHOULD-BE-CHART' \
95
+ 'imbalance.md=CARRIER-IMBALANCE,ICD203-9-SHOULD-BE-CHART' \
96
+ 'fig-dangling.md=FIG-REF-DANGLING' \
97
+ 'fig-orphan.md=FIG-ORPHAN' \
98
+ 'fig-orphan-captioned.md=FIG-ORPHAN' \
99
+ 'empty-header.md=WCAG-131-TABLE' \
100
+ 'wide-table.md=TABLE-WIDE' \
101
+ 'unfilled.md=TABLE-UNFILLED' \
102
+ 'fenced-noise.md='
103
+
104
+ # 无契约时应提示「重复出现的值该成为 token」——独立目录,避免污染契约测试
105
+ t3="$(mktemp -d)"
106
+ # 需要一张「多种颜色各重复多次」的图才触发;就地合成,不入仓
107
+ python3 - "${t3}/multi.svg" <<'SVGEOF'
108
+ import sys
109
+ cols = ["#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00"]
110
+ parts = ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200" role="img" aria-label="many repeated colours"><title>many repeated colours</title><g>']
111
+ y = 5
112
+ for c in cols:
113
+ for _ in range(3):
114
+ parts.append(f'<rect x="10" y="{y}" width="12" height="8" fill="{c}"/>')
115
+ y += 10
116
+ parts.append("</g></svg>")
117
+ open(sys.argv[1], "w").write("".join(parts))
118
+ SVGEOF
119
+ out2="$(python3 "${here}/figure-lint.py" "${t3}"/*.svg 2>/dev/null)"; rc2=$?
120
+ case "${out2}" in *VALUE-SHOULD-BE-TOKEN*) ;; *) bad "无契约且多色重复时未报 VALUE-SHOULD-BE-TOKEN";; esac
121
+ [ "${rc2}" = 1 ] || bad "多色重复用例退出码应为 1(CONTRACT-MISSING 是 ERROR),实得 ${rc2}"
122
+ # 规则写的是「>2 次即应成 token」——单一颜色重复 3 次同样要报,否则规则与实现不一致
123
+ python3 - "${t3}/one.svg" <<'SVGEOF2'
124
+ import sys
125
+ parts = ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200" role="img" aria-label="one repeated colour"><title>one repeated colour</title><g>']
126
+ y = 5
127
+ for _ in range(3):
128
+ parts.append(f'<rect x="10" y="{y}" width="12" height="8" fill="#0072B2"/>')
129
+ y += 10
130
+ parts.append("</g></svg>")
131
+ open(sys.argv[1], "w").write("".join(parts))
132
+ SVGEOF2
133
+ out2b="$(python3 "${here}/figure-lint.py" "${t3}/one.svg" 2>/dev/null)"; rc2b=$?
134
+ case "${out2b}" in *VALUE-SHOULD-BE-TOKEN*) ;; *) bad "单色重复 3 次未报 VALUE-SHOULD-BE-TOKEN(规则与实现不一致)";; esac
135
+ [ "${rc2b}" = 1 ] || bad "单色重复用例退出码应为 1(CONTRACT-MISSING 是 ERROR),实得 ${rc2b}"
136
+ rm -rf "${t3}"
137
+
138
+ # CVD 距离只在契约**显式声明语义色**时测量,且只报数不下判定
139
+ t3b="$(mktemp -d)"
140
+ cp "${here}/tests/svg/cvd-confusable.svg" "${t3b}/"
141
+ cat >"${t3b}/figure-contract.json" <<'JSON'
142
+ { "canvas_ratios": [2.0], "font_scale": [14],
143
+ "color_tokens": {"ink":"#111827","warn":"#B54708","crit":"#B42318","ok":"#009E73","surface":"#FFFFFF"},
144
+ "semantic_colors": {"warning":"#B54708","critical":"#B42318","ok":"#009E73"} }
145
+ JSON
146
+ out3b="$(python3 "${here}/figure-lint.py" "${t3b}/cvd-confusable.svg" 2>/dev/null)"; rc3b=$?
147
+ case "${out3b}" in *CVD-DISTANCE*) ;; *) bad "契约声明语义色后未测量 CVD-DISTANCE";; esac
148
+ case "${out3b}" in *"测量值不是判定"*) ;; *) bad "CVD-DISTANCE 文案未声明这是测量值";; esac
149
+ # 「只报数不设阈」若让退出码变成非零,这句声明就被退出码当场推翻——必须是 INFO 档、退出码 0
150
+ [ "${rc3b}" = 0 ] || bad "只有 CVD-DISTANCE 测量时退出码应为 0(INFO 不阻断),实得 ${rc3b}"
151
+ case "${out3b}" in *INFO*) ;; *) bad "CVD-DISTANCE 未标为 INFO 档";; esac
152
+ # semantic_colors 非法必须报 CONTRACT-INVALID,不得崩溃、也不得静默丢成空集
153
+ for bad_sc in '["#B54708","#B42318"]' '"#B54708"' '{"a":"red","b":"#B42318"}' '{"a":"#123456","b":"#B42318"}'; do
154
+ cat >"${t3b}/figure-contract.json" <<JSON
155
+ { "canvas_ratios": [2.0], "font_scale": [14],
156
+ "color_tokens": {"ink":"#111827","warn":"#B54708","crit":"#B42318","ok":"#009E73","surface":"#FFFFFF"},
157
+ "semantic_colors": ${bad_sc} }
158
+ JSON
159
+ o="$(python3 "${here}/figure-lint.py" "${t3b}/cvd-confusable.svg" 2>&1)"; r=$?
160
+ case "${o}" in *CONTRACT-INVALID*) ;; *) bad "非法 semantic_colors ${bad_sc} 未报 CONTRACT-INVALID";; esac
161
+ case "${o}" in *Traceback*) bad "非法 semantic_colors ${bad_sc} 让检查器崩溃";; esac
162
+ [ "${r}" = 1 ] || bad "非法 semantic_colors ${bad_sc} 应为 ERROR(退出码 1),实得 ${r}"
163
+ done
164
+ # 未声明语义色时不得凭渲染色臆断语义
165
+ out3c="$(python3 "${here}/figure-lint.py" "${here}/tests/svg/cvd-confusable.svg" 2>/dev/null || true)"
166
+ case "${out3c}" in *CVD-*) bad "契约未声明语义色却报了 CVD-*(把装饰色当语义色)";; esac
167
+ rm -rf "${t3b}"
168
+
169
+ # 主题维度:同一套写死的色在另一主题底上会失效;纯黑底另有提示。
170
+ t4="$(mktemp -d)"
171
+ cp "${here}/tests/svg/control.svg" "${t4}/"
172
+ cat >"${t4}/figure-contract.json" <<'JSON'
173
+ { "canvas_ratios": [2.0], "font_scale": [13,14],
174
+ "color_tokens": {"ink":"#111827","primary":"#0072B2","critical":"#D55E00","ok":"#009E73","surface":"#FFFFFF"},
175
+ "themes": { "light": {"surface":"#FFFFFF"}, "dark": {"surface":"#000000"} } }
176
+ JSON
177
+ out4="$(python3 "${here}/figure-lint.py" "${t4}"/control.svg 2>/dev/null)"; rc4=$?
178
+ [ "${rc4}" = 2 ] || bad "主题用例退出码应为 2(仅 WARN),实得 ${rc4}"
179
+ case "${out4}" in *THEME-CONTRAST*) ;; *) bad "深色主题下未报 THEME-CONTRAST";; esac
180
+ case "${out4}" in *THEME-PURE-BLACK*) ;; *) bad "纯黑主题底未报 THEME-PURE-BLACK";; esac
181
+ # 主题底色不是合法 #RRGGBB 时必须报契约非法——否则该主题被静默跳过 = 假绿
182
+ cat >"${t4}/figure-contract.json" <<'JSON'
183
+ { "canvas_ratios": [2.0], "font_scale": [13,14],
184
+ "color_tokens": {"ink":"#111827","primary":"#0072B2","critical":"#D55E00","ok":"#009E73","surface":"#FFFFFF"},
185
+ "themes": { "light": {"surface":"#FFFFFF"}, "dark": {"surface":"black"} } }
186
+ JSON
187
+ out4b="$(python3 "${here}/figure-lint.py" "${t4}"/control.svg 2>/dev/null)"; rc4b=$?
188
+ case "${out4b}" in *CONTRACT-INVALID*) ;; *) bad "themes.dark.surface 非十六进制未报 CONTRACT-INVALID(静默跳过=假绿)";; esac
189
+ [ "${rc4b}" = 1 ] || bad "契约非法应为 ERROR(退出码 1),实得 ${rc4b}"
190
+ cat >"${t4}/figure-contract.json" <<'JSON'
191
+ { "canvas_ratios": [2.0], "font_scale": [13,14],
192
+ "color_tokens": {"ink":"#111827","primary":"#0072B2","critical":"#D55E00","ok":"#009E73","surface":"#FFFFFF"},
193
+ "themes": { "light": {"surface":"#FFFFFF"}, "dark": {"surface":[255]} } }
194
+ JSON
195
+ out4c="$(python3 "${here}/figure-lint.py" "${t4}"/control.svg 2>/dev/null || true)"
196
+ case "${out4c}" in *CONTRACT-INVALID*) ;; *) bad "themes.dark.surface 非字符串未报 CONTRACT-INVALID";; esac
197
+
198
+ # 底色解析不可信时,主题对比度不得凭全局底色判——必须显式报未判定
199
+ t4u="$(mktemp -d)"
200
+ cp "${here}/tests/svg/transformed.svg" "${t4u}/"
201
+ cat >"${t4u}/figure-contract.json" <<'JSON'
202
+ { "canvas_ratios": [2.0], "font_scale": [13,14],
203
+ "color_tokens": {"ink":"#111827","primary":"#0072B2","surface":"#FFFFFF"},
204
+ "themes": { "light": {"surface":"#FFFFFF"}, "dark": {"surface":"#0B0F19"} } }
205
+ JSON
206
+ out4u="$(python3 "${here}/figure-lint.py" "${t4u}/transformed.svg" 2>/dev/null || true)"
207
+ case "${out4u}" in *THEME-UNASSESSED*) ;; *) bad "几何不可信时未报 THEME-UNASSESSED";; esac
208
+ case "${out4u}" in *THEME-CONTRAST*) bad "几何不可信时仍判 THEME-CONTRAST(拿不可信底色判出的假红)";; esac
209
+ rm -rf "${t4u}"
210
+
211
+ # fill="none" 的描边框不是底板:包住低对比文字的边框不得豁免 THEME-CONTRAST
212
+ t4s="$(mktemp -d)"
213
+ cat >"${t4s}/stroked.svg" <<'SVG'
214
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200" role="img" aria-label="stroked box"><title>stroked box</title><g>
215
+ <rect x="0" y="0" width="400" height="200" fill="#FFFFFF"/>
216
+ <rect x="20" y="20" width="200" height="60" fill="none" stroke="#0072B2"/>
217
+ <text x="30" y="55" font-size="14" fill="#111827">深色小字</text>
218
+ </g></svg>
219
+ SVG
220
+ cat >"${t4s}/figure-contract.json" <<'JSON'
221
+ { "canvas_ratios": [2.0], "font_scale": [14],
222
+ "color_tokens": {"ink":"#111827","primary":"#0072B2","surface":"#FFFFFF"},
223
+ "themes": { "light": {"surface":"#FFFFFF"}, "dark": {"surface":"#0B0F19"} } }
224
+ JSON
225
+ out4s="$(python3 "${here}/figure-lint.py" "${t4s}/stroked.svg" 2>/dev/null || true)"
226
+ case "${out4s}" in *THEME-CONTRAST*) ;; *) bad "fill=none 的描边框被当成底板,豁免了 THEME-CONTRAST";; esac
227
+ rm -rf "${t4s}"
228
+
229
+ # 两端都没箭头的无向连接不得按 d 的书写顺序造出方向
230
+ t5="$(mktemp -d)"
231
+ cat >"${t5}/undirected.svg" <<'SVG'
232
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200" role="img" aria-label="undirected"><title>undirected</title>
233
+ <defs><marker id="a" refX="10" refY="6"><path d="M1 1L11 6L1 11Z" fill="#0072B2"/></marker></defs><g>
234
+ <rect x="0" y="0" width="400" height="200" fill="#FFFFFF"/>
235
+ <path d="M10 20L200 20" fill="none" stroke="#0072B2"/>
236
+ <path d="M200 60L10 60" fill="none" stroke="#0072B2"/>
237
+ <path d="M10 100L200 100" fill="none" stroke="#0072B2"/>
238
+ <path d="M200 140L10 140" fill="none" stroke="#0072B2"/>
239
+ </g></svg>
240
+ SVG
241
+ out5="$(python3 "${here}/figure-lint.py" "${t5}/undirected.svg" 2>/dev/null || true)"
242
+ case "${out5}" in *FLOW-DIRECTION-MIXED*) bad "无箭头的连接被按 d 书写顺序判成混合流向";; esac
243
+ rm -rf "${t5}"
244
+
245
+ # 主题相符时不得误报
246
+ cat >"${t4}/figure-contract.json" <<'JSON'
247
+ { "canvas_ratios": [2.0], "font_scale": [13,14],
248
+ "color_tokens": {"ink":"#111827","primary":"#0072B2","critical":"#D55E00","ok":"#009E73","surface":"#FFFFFF"},
249
+ "themes": { "light": {"surface":"#FFFFFF"} } }
250
+ JSON
251
+ out5="$(python3 "${here}/figure-lint.py" "${t4}"/control.svg 2>/dev/null || true)"
252
+ case "${out5}" in *THEME-CONTRAST*) bad "单一浅色主题下误报 THEME-CONTRAST";; esac
253
+ rm -rf "${t4}"
254
+
255
+ # ---- 契约符合性:缺失 / 相符 / 不符 / 损坏 / 多目录 ----
256
+ note "== figure-lint 契约符合性 =="
257
+ tmp="$(mktemp -d)"; t2="$(mktemp -d)"
258
+ trap 'rm -rf "${tmp}" "${t2}"' EXIT
259
+ cp "${here}/tests/svg/control.svg" "${tmp}/"
260
+
261
+ out="$(python3 "${here}/figure-lint.py" "${tmp}" 2>/dev/null || true)"
262
+ case "${out}" in *CONTRACT-MISSING*) ;; *) bad "契约缺失时未报 CONTRACT-MISSING";; esac
263
+
264
+
265
+ # 相符:取 control.svg 的真实取值,必须干净(写负例前先核对 fixture 真值,
266
+ # 否则会写出一份"其实相符"的契约让断言永远绿——本轮实际踩过)。
267
+ cat >"${tmp}/figure-contract.json" <<'JSON'
268
+ { "canvas_ratios": [2.0], "font_scale": [13, 14],
269
+ "color_tokens": { "ink": "#111827", "primary": "#0072B2", "critical": "#D55E00",
270
+ "ok": "#009E73", "surface": "#FFFFFF" } }
271
+ JSON
272
+ out="$(python3 "${here}/figure-lint.py" "${tmp}" 2>/dev/null || true)"
273
+ case "${out}" in *CONTRACT-MISSING*) bad "契约存在时仍报 CONTRACT-MISSING";; esac
274
+ case "${out}" in
275
+ *CONTRACT-RATIO*|*CONTRACT-FONT-SCALE*|*CONTRACT-COLOR-TOKEN*)
276
+ bad "契约与 fixture 相符却报了偏离(假阳性)";;
277
+ esac
278
+
279
+ # 不符:三维各自断言。写成 OR(三选一命中即过)会让其余两维永久失去覆盖——
280
+ # 突变实测过:删掉 CONTRACT-RATIO 后套件仍绿,因为 FONT-SCALE 顶上了。
281
+ cat >"${tmp}/figure-contract.json" <<'JSON'
282
+ { "canvas_ratios": [1.778], "font_scale": [99], "color_tokens": { "ink": "#000000" } }
283
+ JSON
284
+ out="$(python3 "${here}/figure-lint.py" "${tmp}" 2>/dev/null || true)"
285
+ for want in CONTRACT-RATIO CONTRACT-FONT-SCALE CONTRACT-COLOR-TOKEN; do
286
+ case "${out}" in
287
+ *"${want}"*) ;;
288
+ *) bad "契约不符时未报 ${want}——该维度的契约检查形同虚设";;
289
+ esac
290
+ done
291
+
292
+ # 损坏:语法错与类型非法都必须报 CONTRACT-INVALID 且不得崩溃。
293
+ printf '{ "canvas_ratios": [1.778,, }' >"${tmp}/figure-contract.json"
294
+ out="$(python3 "${here}/figure-lint.py" "${tmp}" 2>/dev/null || true)"
295
+ case "${out}" in *CONTRACT-INVALID*) ;; *) bad "契约语法损坏时未报 CONTRACT-INVALID";; esac
296
+
297
+ cat >"${tmp}/figure-contract.json" <<'JSON'
298
+ { "canvas_ratios": "not-a-list", "font_scale": [ {"x": 1} ], "color_tokens": 42 }
299
+ JSON
300
+ out="$(python3 "${here}/figure-lint.py" "${tmp}" 2>/dev/null || true)"
301
+ case "${out}" in *CONTRACT-INVALID*) ;; *) bad "契约类型非法时未报 CONTRACT-INVALID(或已崩溃)";; esac
302
+
303
+ # 多目录:两棵树各有自己的契约,必须各按各的最近契约判,不能用第一棵的套第二棵。
304
+ mkdir -p "${t2}/a" "${t2}/b"
305
+ cp "${here}/tests/svg/control.svg" "${t2}/a/"
306
+ cp "${here}/tests/svg/control.svg" "${t2}/b/"
307
+ cat >"${t2}/a/figure-contract.json" <<'JSON'
308
+ { "canvas_ratios": [2.0], "font_scale": [13, 14],
309
+ "color_tokens": { "ink": "#111827", "primary": "#0072B2", "critical": "#D55E00",
310
+ "ok": "#009E73", "surface": "#FFFFFF" } }
311
+ JSON
312
+ cat >"${t2}/b/figure-contract.json" <<'JSON'
313
+ { "canvas_ratios": [1.5], "font_scale": [13, 14],
314
+ "color_tokens": { "ink": "#111827", "primary": "#0072B2", "critical": "#D55E00",
315
+ "ok": "#009E73", "surface": "#FFFFFF" } }
316
+ JSON
317
+ out="$(python3 "${here}/figure-lint.py" "${t2}/a/control.svg" "${t2}/b/control.svg" --json 2>/dev/null || true)"
318
+ printf '%s' "${out}" | python3 -c '
319
+ import json, sys, os
320
+ d = json.load(sys.stdin)
321
+ per = {os.path.basename(os.path.dirname(k)): sorted({x["code"] for x in v["findings"]})
322
+ for k, v in d["files"].items()}
323
+ sys.exit(0 if per.get("a") == [] and "CONTRACT-RATIO" in per.get("b", []) else 1)
324
+ ' 2>/dev/null || bad "多目录时未按各自最近的契约判定(a 应干净、b 应报 CONTRACT-RATIO)"
325
+
326
+ # ---- 退出码契约回归 ----
327
+ # 两个 CLI 都声明「ERROR=1 / 仅 WARN=2 / 干净=0」,普通与 --json 模式一致。
328
+ # 此前套件全程忽略退出码(|| true),把 1 改成 0 也不会转红——契约无保护。
329
+ note "== 退出码契约(0 干净 / 2 仅 WARN / 1 有 ERROR)=="
330
+ assert_rc() { # <期望码> <说明> <命令...>
331
+ "${@:3}" >/dev/null 2>&1
332
+ local rc=$?
333
+ [ "${rc}" -eq "$1" ] || bad "退出码契约:$2 期望 $1 实得 ${rc}"
334
+ }
335
+ for mode in "" "--json"; do
336
+ label="${mode:-plain}"
337
+ assert_rc 0 "figure-lint 干净(${label})" python3 "${here}/figure-lint.py" "${here}/tests/svg/control.svg" ${mode}
338
+ assert_rc 1 "figure-lint 有 ERROR(${label})" python3 "${here}/figure-lint.py" "${here}/tests/svg/no-title.svg" ${mode}
339
+ assert_rc 2 "figure-lint 仅 WARN(${label})" python3 "${here}/figure-lint.py" "${here}/tests/svg/transformed.svg" ${mode}
340
+ assert_rc 0 "doc-lint 干净(${label})" python3 "${here}/doc-lint.py" "${here}/tests/doc/control.md" ${mode}
341
+ assert_rc 1 "doc-lint 有 ERROR(${label})" python3 "${here}/doc-lint.py" "${here}/tests/doc/fig-dangling.md" ${mode}
342
+ assert_rc 2 "doc-lint 仅 WARN(${label})" python3 "${here}/doc-lint.py" "${here}/tests/doc/no-unit.md" ${mode}
343
+ done
344
+
345
+ # ---- 读取失败不得中断整批 ----
346
+ # 非 UTF-8 fixture **不入仓**:提交进去会打断全仓扫描器(R0 审计实测 sed 报
347
+ # illegal byte sequence)。临时生成即可。
348
+ note "== 读取失败隔离 =="
349
+ python3 -c "open('${tmp}/non-utf8.svg','wb').write(b'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 10 10\"><title>\xff\xfe</title></svg>')"
350
+ cp "${here}/tests/svg/control.svg" "${tmp}/ok.svg"
351
+ cp "${here}/tests/svg/figure-contract.json" "${tmp}/"
352
+ mixed="$(python3 "${here}/figure-lint.py" "${tmp}/non-utf8.svg" "${tmp}/ok.svg" --json 2>/dev/null)"
353
+ printf '%s' "${mixed}" | python3 -c '
354
+ import json, sys, os
355
+ d = json.load(sys.stdin)
356
+ per = {os.path.basename(k): sorted({x["code"] for x in v["findings"]}) for k, v in d["files"].items()}
357
+ ok = per.get("non-utf8.svg") == ["READ"] and per.get("ok.svg") == []
358
+ sys.exit(0 if ok else 1)
359
+ ' 2>/dev/null || bad "坏文件未被隔离:期望 non-utf8.svg=[READ] 且 ok.svg 干净,实得 ${mixed}"
360
+
361
+ # doc-lint 同样:坏 .md 报 READ 并继续,不得静默判干净。
362
+ python3 -c "open('${tmp}/bad.md','wb').write(b'# t\n\xff\xfe\n')"
363
+ cp "${here}/tests/doc/control.md" "${tmp}/good.md"
364
+ dmixed="$(python3 "${here}/doc-lint.py" "${tmp}/bad.md" "${tmp}/good.md" --json 2>/dev/null)"
365
+ printf '%s' "${dmixed}" | python3 -c '
366
+ import json, sys, os
367
+ d = json.load(sys.stdin)
368
+ per = {os.path.basename(k): sorted({x["code"] for x in v["findings"]}) for k, v in d.get("files", d).items()}
369
+ sys.exit(0 if per.get("bad.md") == ["READ"] and per.get("good.md") == [] else 1)
370
+ ' 2>/dev/null || bad "doc-lint 坏文件未被隔离:期望 bad.md=[READ] 且 good.md 干净,实得 ${dmixed}"
371
+
372
+ if [ "${fail}" -eq 0 ]; then
373
+ note "OK: figure-lint / doc-lint 逐谓词差分与契约五态全部符合预期"
374
+ fi
375
+ exit "${fail}"
@@ -0,0 +1,10 @@
1
+ # 正常文档
2
+
3
+ ## 一节
4
+
5
+ | 项 | 值(元) | 说明 |
6
+ |---|---|---|
7
+ | A | 1 | 甲 |
8
+ | B | 2 | 乙 |
9
+
10
+ ![图](fig.png)
@@ -0,0 +1,6 @@
1
+ # 文档
2
+
3
+ | | |
4
+ |---|---|
5
+ | 甲 | 1 |
6
+ | 乙 | 2 |
@@ -0,0 +1,13 @@
1
+ # 文档
2
+
3
+ **小节0**
4
+
5
+ **小节1**
6
+
7
+ **小节2**
8
+
9
+ **小节3**
10
+
11
+ **小节4**
12
+
13
+ **小节5**
@@ -0,0 +1,14 @@
1
+ # 围栏代码块不参与结构分析
2
+
3
+ 下面这段全在围栏里,任何谓词都不该被它触发。
4
+
5
+ ```
6
+ ── Report ────────────────────────────────────────────────
7
+ # 这是示例里的井号行,不是本文档的标题
8
+ | 键 | |
9
+ |---|---|
10
+ | a | TBD |
11
+ 见图 9 所示。
12
+ ```
13
+
14
+ 正文到此。
@@ -0,0 +1,5 @@
1
+ # 文档
2
+
3
+ 见图 3 所示。
4
+
5
+ ![只有一张](fig.png)
@@ -0,0 +1,11 @@
1
+ # 文档
2
+
3
+ 正文只提到图 1。
4
+
5
+ ![一](fig.png)
6
+
7
+ *图 1:第一张*
8
+
9
+ ![二](fig.png)
10
+
11
+ *图 2:第二张,正文从未引用*
@@ -0,0 +1,9 @@
1
+ # 文档
2
+
3
+ 正文完全不提图。
4
+
5
+ ![一](fig.png)
6
+
7
+ ![二](fig.png)
8
+
9
+ ![三](fig.png)
@@ -0,0 +1,41 @@
1
+ # 载体错配
2
+
3
+ 八张表、零图,且其中一张是量级对比表——那张该出图。
4
+
5
+ | 组 | 人数 |
6
+ |---|---|
7
+ | G1 | 7 |
8
+ | G2 | 14 |
9
+ | G3 | 21 |
10
+ | G4 | 28 |
11
+ | G5 | 35 |
12
+ | G6 | 42 |
13
+ | G7 | 49 |
14
+
15
+ | 键 | 含义 |
16
+ |---|---|
17
+ | k0 | 值0 |
18
+
19
+ | 键 | 含义 |
20
+ |---|---|
21
+ | k1 | 值1 |
22
+
23
+ | 键 | 含义 |
24
+ |---|---|
25
+ | k2 | 值2 |
26
+
27
+ | 键 | 含义 |
28
+ |---|---|
29
+ | k3 | 值3 |
30
+
31
+ | 键 | 含义 |
32
+ |---|---|
33
+ | k4 | 值4 |
34
+
35
+ | 键 | 含义 |
36
+ |---|---|
37
+ | k5 | 值5 |
38
+
39
+ | 键 | 含义 |
40
+ |---|---|
41
+ | k6 | 值6 |
@@ -0,0 +1,8 @@
1
+ # 文档
2
+
3
+ | 站点 | 占比 | 说明 |
4
+ |---|---|---|
5
+ | 甲 | 60 | x |
6
+ | 乙 | 58 | y |
7
+ | 丙 | 37 | z |
8
+ | 丁 | 32 | w |
@@ -0,0 +1,11 @@
1
+ # 文档
2
+
3
+ | 组 | 人数 |
4
+ |---|---|
5
+ | G1 | 7 |
6
+ | G2 | 14 |
7
+ | G3 | 21 |
8
+ | G4 | 28 |
9
+ | G5 | 35 |
10
+ | G6 | 42 |
11
+ | G7 | 49 |
@@ -0,0 +1,35 @@
1
+ # 查询表文档
2
+
3
+ 八张纯查询表,不含量级对比,不该被要求出图。
4
+
5
+ | 键 | 含义 |
6
+ |---|---|
7
+ | a | 甲 |
8
+
9
+ | 键 | 含义 |
10
+ |---|---|
11
+ | b | 乙 |
12
+
13
+ | 键 | 含义 |
14
+ |---|---|
15
+ | c | 丙 |
16
+
17
+ | 键 | 含义 |
18
+ |---|---|
19
+ | d | 丁 |
20
+
21
+ | 键 | 含义 |
22
+ |---|---|
23
+ | e | 戊 |
24
+
25
+ | 键 | 含义 |
26
+ |---|---|
27
+ | f | 己 |
28
+
29
+ | 键 | 含义 |
30
+ |---|---|
31
+ | g | 庚 |
32
+
33
+ | 键 | 含义 |
34
+ |---|---|
35
+ | h | 辛 |