trae-coding-engine 0.1.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.
- package/README.md +91 -0
- package/bin/coding-engine.js +16 -0
- package/lib/cli.js +86 -0
- package/lib/index.js +6 -0
- package/lib/setup.js +290 -0
- package/package.json +32 -0
- package/templates/.agents/skills/coding-architecture/SKILL.md +347 -0
- package/templates/.agents/skills/coding-code-review/SKILL.md +95 -0
- package/templates/.agents/skills/coding-cve-remediation/SKILL.md +163 -0
- package/templates/.agents/skills/coding-db-schema/SKILL.md +273 -0
- package/templates/.agents/skills/coding-deploy-config/SKILL.md +129 -0
- package/templates/.agents/skills/coding-docs-audit/SKILL.md +285 -0
- package/templates/.agents/skills/coding-docs-audit-autofix/SKILL.md +33 -0
- package/templates/.agents/skills/coding-http-api/SKILL.md +269 -0
- package/templates/.agents/skills/coding-issue-autodev/SKILL.md +172 -0
- package/templates/.agents/skills/coding-merge-request/SKILL.md +199 -0
- package/templates/.agents/skills/coding-observability/SKILL.md +193 -0
- package/templates/.agents/skills/coding-refactor-insight/SKILL.md +89 -0
- package/templates/.agents/skills/coding-release-merge/SKILL.md +224 -0
- package/templates/.agents/skills/coding-runtime-adapter/SKILL.md +115 -0
- package/templates/.agents/skills/coding-testing/SKILL.md +169 -0
- package/templates/AGENTS.md +179 -0
- package/templates/MR-Template-Default.md +33 -0
- package/templates/Makefile +370 -0
- package/templates/REGISTRY.md +53 -0
- package/templates/scripts/check-adr.sh +283 -0
- package/templates/scripts/check-comment-i18n.py +209 -0
- package/templates/scripts/check-comment-i18n.sh +38 -0
- package/templates/scripts/check-doc-refs.sh +40 -0
- package/templates/scripts/check-docs.sh +103 -0
- package/templates/scripts/check-go-names.sh +59 -0
- package/templates/scripts/check-iam-actions.py +126 -0
- package/templates/scripts/check-migration-sql-immutability.sh +232 -0
- package/templates/scripts/check-mr-desc.sh +165 -0
- package/templates/scripts/check-mr-size.sh +128 -0
- package/templates/scripts/check-mr-title.sh +123 -0
- package/templates/scripts/check-openapi.py +221 -0
- package/templates/scripts/check-rdb-structured-queries.sh +98 -0
- package/templates/scripts/check-repository-transactions.sh +100 -0
- package/templates/scripts/check-runbooks.sh +229 -0
- package/templates/scripts/check-skill-router-sync.py +331 -0
- package/templates/scripts/check-skills.sh +243 -0
- package/templates/scripts/docs-audit-to-issues.py +373 -0
- package/templates/scripts/docs-verification-reminder.sh +63 -0
- package/templates/scripts/find-ci-failures.sh +133 -0
- package/templates/scripts/find-stale-mrs.sh +72 -0
- package/templates/scripts/gen-adr-index.sh +122 -0
- package/templates/scripts/open-mr.sh +536 -0
- package/templates/scripts/regen-agent-md.sh +149 -0
- package/templates/scripts/run-mr-hygiene.sh +140 -0
- package/templates/scripts/runbook.sh +91 -0
- package/templates/scripts/self-maintenance-digest.py +125 -0
- package/templates/scripts/send-self-maintenance-lark-card.py +116 -0
- package/templates/scripts/skill-graph.sh +114 -0
- package/templates/scripts/skill-impact.sh +134 -0
- package/templates/scripts/skill-propose.sh +302 -0
- package/templates/scripts/skill-router-hook.sh +152 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Verify every ADR under docs/adr/ is in a terminal Status.
|
|
3
|
+
# See docs/adr/CONVENTIONS.md.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
cd "$(dirname "$0")/.."
|
|
7
|
+
|
|
8
|
+
# shellcheck source=scripts/lib/adr-meta.sh
|
|
9
|
+
. scripts/lib/adr-meta.sh
|
|
10
|
+
ADR_DIR="${ADR_DIR:-docs/adr}"
|
|
11
|
+
|
|
12
|
+
# Grandfathered violations — historical ADRs that predate the convention.
|
|
13
|
+
# Each entry MUST have a corresponding "现存待清理项" row in
|
|
14
|
+
# docs/adr/CONVENTIONS.md so cleanup pressure is visible.
|
|
15
|
+
GRANDFATHERED=()
|
|
16
|
+
|
|
17
|
+
is_grandfathered() {
|
|
18
|
+
local f=$1
|
|
19
|
+
for g in "${GRANDFATHERED[@]+"${GRANDFATHERED[@]}"}"; do
|
|
20
|
+
[ "$f" = "$g" ] && return 0
|
|
21
|
+
done
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Intentionally-unused ADR numbers. Each entry needs a matching note in
|
|
26
|
+
# docs/adr/CONVENTIONS.md (现存待清理项 / KNOWN_GAPS).
|
|
27
|
+
KNOWN_GAPS=(0011)
|
|
28
|
+
# Test override hook (space-separated). Empty string means "no gaps allowed".
|
|
29
|
+
if [ "${KNOWN_GAPS_OVERRIDE+set}" = set ]; then
|
|
30
|
+
# shellcheck disable=SC2206
|
|
31
|
+
KNOWN_GAPS=(${KNOWN_GAPS_OVERRIDE})
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
is_known_gap() {
|
|
35
|
+
local n=$1 g
|
|
36
|
+
for g in "${KNOWN_GAPS[@]+"${KNOWN_GAPS[@]}"}"; do
|
|
37
|
+
[ "$n" = "$g" ] && return 0
|
|
38
|
+
done
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Topic taxonomy comes from $ADR_TOPICS, defined once in scripts/lib/adr-meta.sh
|
|
43
|
+
# (sourced above) so the linter and the index generator can't drift. The linter
|
|
44
|
+
# validates **Topic** against this list, never against AGENTS.md §Skill Router
|
|
45
|
+
# directly (the 7 path-skills are coarser). Topic→skill mapping is documented in
|
|
46
|
+
# docs/adr/CONVENTIONS.md.
|
|
47
|
+
|
|
48
|
+
VIOLATIONS=0
|
|
49
|
+
WARNINGS=0
|
|
50
|
+
# Back-citation enforcement mode: "warn" (Phase 2) or "hard" (Phase 3).
|
|
51
|
+
BACKCITE_MODE="${BACKCITE_MODE:-hard}"
|
|
52
|
+
GRANDFATHERED_HITS=0
|
|
53
|
+
for f in "$ADR_DIR"/[0-9]*.md; do
|
|
54
|
+
[ -f "$f" ] || continue
|
|
55
|
+
status=$(awk '/^\*\*Status\*\*:/ { sub(/^\*\*Status\*\*:[[:space:]]*/, ""); print; exit }' "$f")
|
|
56
|
+
|
|
57
|
+
case "$status" in
|
|
58
|
+
Accepted|Deprecated|Withdrawn)
|
|
59
|
+
;;
|
|
60
|
+
"Superseded by "*)
|
|
61
|
+
target="${status#Superseded by }"
|
|
62
|
+
if printf '%s' "$target" | grep -qE '^[0-9]{4}$'; then
|
|
63
|
+
if ! ls "$ADR_DIR/${target}-"*.md >/dev/null 2>&1; then
|
|
64
|
+
echo " [VIOLATION] $f — Superseded by $target but no $ADR_DIR/${target}-*.md exists"
|
|
65
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
66
|
+
fi
|
|
67
|
+
else
|
|
68
|
+
echo " [VIOLATION] $f — Superseded by '$target' is not a 4-digit ADR number (use 'Superseded by NNNN'; if no successor ADR, use Deprecated)"
|
|
69
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
70
|
+
fi
|
|
71
|
+
;;
|
|
72
|
+
Proposed)
|
|
73
|
+
if is_grandfathered "$f"; then
|
|
74
|
+
echo " [GRANDFATHERED] $f — Status=Proposed (cleanup tracked in CONVENTIONS.md)"
|
|
75
|
+
GRANDFATHERED_HITS=$((GRANDFATHERED_HITS + 1))
|
|
76
|
+
else
|
|
77
|
+
echo " [VIOLATION] $f — Status=Proposed not allowed on main; flip to Accepted before merging"
|
|
78
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
79
|
+
fi
|
|
80
|
+
;;
|
|
81
|
+
"")
|
|
82
|
+
if is_grandfathered "$f"; then
|
|
83
|
+
echo " [GRANDFATHERED] $f — missing **Status** header (cleanup tracked in CONVENTIONS.md)"
|
|
84
|
+
GRANDFATHERED_HITS=$((GRANDFATHERED_HITS + 1))
|
|
85
|
+
else
|
|
86
|
+
echo " [VIOLATION] $f — missing '**Status**:' header"
|
|
87
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
*)
|
|
91
|
+
echo " [VIOLATION] $f — unknown Status: '$status'"
|
|
92
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
done
|
|
96
|
+
|
|
97
|
+
# Reference integrity: every ADR-NNNN / ADR NNNN token inside the ADR set must
|
|
98
|
+
# resolve to an existing $ADR_DIR/NNNN-*.md. (Repo-wide scanning is intentionally
|
|
99
|
+
# left to a future widening; ADR-internal cross-refs are the highest-value guard.)
|
|
100
|
+
check_adr_refs() {
|
|
101
|
+
local f tok num
|
|
102
|
+
while IFS= read -r f; do
|
|
103
|
+
[ -f "$f" ] || continue
|
|
104
|
+
while IFS= read -r tok; do
|
|
105
|
+
num="$(printf '%s' "$tok" | grep -oE '[0-9]{4}')"
|
|
106
|
+
[ -z "$num" ] && continue
|
|
107
|
+
if ! ls "$ADR_DIR/${num}-"*.md >/dev/null 2>&1; then
|
|
108
|
+
echo " [VIOLATION] $f — references ${tok} but no $ADR_DIR/${num}-*.md exists"
|
|
109
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
110
|
+
fi
|
|
111
|
+
done < <(grep -oE 'ADR[- ][0-9]{4}' "$f" | sort -u)
|
|
112
|
+
done < <(adr_files)
|
|
113
|
+
}
|
|
114
|
+
check_adr_refs
|
|
115
|
+
|
|
116
|
+
# Numbering: contiguous from the lowest present number to the highest; any
|
|
117
|
+
# missing number must be allowlisted in KNOWN_GAPS.
|
|
118
|
+
check_numbering() {
|
|
119
|
+
local nums max min n
|
|
120
|
+
nums="$(adr_files | while IFS= read -r f; do adr_num "$f"; done | sort -u)"
|
|
121
|
+
[ -z "$nums" ] && return 0
|
|
122
|
+
min="$(printf '%s\n' "$nums" | head -1)"
|
|
123
|
+
max="$(printf '%s\n' "$nums" | tail -1)"
|
|
124
|
+
for n in $(seq "$((10#$min))" "$((10#$max))"); do
|
|
125
|
+
local padded; padded="$(printf '%04d' "$n")"
|
|
126
|
+
if ! printf '%s\n' "$nums" | grep -qx "$padded"; then
|
|
127
|
+
if is_known_gap "$padded"; then
|
|
128
|
+
echo " [KNOWN_GAP] $padded — intentionally unused (tracked in CONVENTIONS.md)"
|
|
129
|
+
else
|
|
130
|
+
echo " [VIOLATION] missing ADR $padded — add the file or allowlist it in KNOWN_GAPS"
|
|
131
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
done
|
|
135
|
+
}
|
|
136
|
+
check_numbering
|
|
137
|
+
|
|
138
|
+
check_topics() {
|
|
139
|
+
local f t ok w
|
|
140
|
+
while IFS= read -r f; do
|
|
141
|
+
[ -f "$f" ] || continue
|
|
142
|
+
t="$(adr_field "$f" Topic)"
|
|
143
|
+
if [ -z "$t" ]; then
|
|
144
|
+
echo " [VIOLATION] $f — missing '**Topic**:' header"
|
|
145
|
+
VIOLATIONS=$((VIOLATIONS + 1)); continue
|
|
146
|
+
fi
|
|
147
|
+
ok=0
|
|
148
|
+
for w in $ADR_TOPICS; do [ "$t" = "$w" ] && ok=1 && break; done
|
|
149
|
+
if [ "$ok" = 0 ]; then
|
|
150
|
+
echo " [VIOLATION] $f — Topic '$t' not in taxonomy ($ADR_TOPICS)"
|
|
151
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
152
|
+
fi
|
|
153
|
+
done < <(adr_files)
|
|
154
|
+
}
|
|
155
|
+
check_topics
|
|
156
|
+
|
|
157
|
+
# Keep the "valid topics" list in docs/adr/CONVENTIONS.md (the block between the
|
|
158
|
+
# `adr-topics` markers) in EXACT set agreement with $ADR_TOPICS — both ways: a
|
|
159
|
+
# topic in ADR_TOPICS but absent from the documented block, OR a documented topic
|
|
160
|
+
# that is not a valid taxonomy value, is a violation. Only the marker block is
|
|
161
|
+
# parsed (the Topic→skill routing table elsewhere uses the same backticked names
|
|
162
|
+
# but must not count). Doc path is overridable for tests; default is the real one.
|
|
163
|
+
check_topic_doc_sync() {
|
|
164
|
+
# Doc path is fixed in production; only a dedicated, clearly test-scoped var can
|
|
165
|
+
# repoint it (same discipline as ADR_TOPICS_TEST_OVERRIDE) — a stray env var
|
|
166
|
+
# must not be able to silently bypass this gate.
|
|
167
|
+
local conv="${ADR_CONV_DOC_TEST_OVERRIDE:-docs/adr/CONVENTIONS.md}" t
|
|
168
|
+
if [ ! -f "$conv" ]; then
|
|
169
|
+
echo " [VIOLATION] $conv — conventions doc not found; cannot verify taxonomy doc-sync"
|
|
170
|
+
VIOLATIONS=$((VIOLATIONS + 1)); return
|
|
171
|
+
fi
|
|
172
|
+
# Marker block must be well-formed first: exactly one start, exactly one end,
|
|
173
|
+
# end after start. Otherwise the awk extractor would run to EOF and harvest
|
|
174
|
+
# topics from outside the intended block (e.g. the routing table), letting a
|
|
175
|
+
# malformed/unclosed block pass doc-sync silently.
|
|
176
|
+
local n_start n_end l_start l_end
|
|
177
|
+
# `|| true`: grep -c exits 1 on zero matches, which under set -e would abort
|
|
178
|
+
# the script before we can report the malformed-block violation.
|
|
179
|
+
n_start="$(grep -c '<!-- adr-topics:start' "$conv" || true)"
|
|
180
|
+
n_end="$(grep -c '<!-- adr-topics:end' "$conv" || true)"
|
|
181
|
+
if [ "$n_start" != 1 ] || [ "$n_end" != 1 ]; then
|
|
182
|
+
echo " [VIOLATION] $conv — adr-topics marker block malformed (need exactly one start + one end; got start=$n_start end=$n_end)"
|
|
183
|
+
VIOLATIONS=$((VIOLATIONS + 1)); return
|
|
184
|
+
fi
|
|
185
|
+
l_start="$(grep -n '<!-- adr-topics:start' "$conv" | cut -d: -f1)"
|
|
186
|
+
l_end="$(grep -n '<!-- adr-topics:end' "$conv" | cut -d: -f1)"
|
|
187
|
+
if [ "$l_end" -le "$l_start" ]; then
|
|
188
|
+
echo " [VIOLATION] $conv — adr-topics:end must come after adr-topics:start"
|
|
189
|
+
VIOLATIONS=$((VIOLATIONS + 1)); return
|
|
190
|
+
fi
|
|
191
|
+
local doc_topics want
|
|
192
|
+
doc_topics="$(awk '
|
|
193
|
+
/<!-- adr-topics:start/ { on=1; next }
|
|
194
|
+
/<!-- adr-topics:end/ { on=0 }
|
|
195
|
+
on { while (match($0, /`[a-z0-9-]+`/)) { print substr($0, RSTART+1, RLENGTH-2); $0 = substr($0, RSTART+RLENGTH) } }
|
|
196
|
+
' "$conv" | sort -u)"
|
|
197
|
+
if [ -z "$doc_topics" ]; then
|
|
198
|
+
echo " [VIOLATION] $conv — missing the adr-topics marker block; cannot verify taxonomy doc-sync"
|
|
199
|
+
VIOLATIONS=$((VIOLATIONS + 1)); return
|
|
200
|
+
fi
|
|
201
|
+
want="$(printf '%s\n' $ADR_TOPICS | sort -u)"
|
|
202
|
+
while IFS= read -r t; do
|
|
203
|
+
[ -z "$t" ] && continue
|
|
204
|
+
printf '%s\n' "$doc_topics" | grep -qx "$t" || {
|
|
205
|
+
echo " [VIOLATION] $conv — taxonomy topic '$t' (in ADR_TOPICS) is missing from the documented list"
|
|
206
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
207
|
+
}
|
|
208
|
+
done <<< "$want"
|
|
209
|
+
while IFS= read -r t; do
|
|
210
|
+
[ -z "$t" ] && continue
|
|
211
|
+
printf '%s\n' "$want" | grep -qx "$t" || {
|
|
212
|
+
echo " [VIOLATION] $conv — documented topic '$t' is not a valid taxonomy value (not in ADR_TOPICS)"
|
|
213
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
214
|
+
}
|
|
215
|
+
done <<< "$doc_topics"
|
|
216
|
+
}
|
|
217
|
+
check_topic_doc_sync
|
|
218
|
+
|
|
219
|
+
# Back-citation: if A is "Superseded by B", B's body must cite A (its 4-digit
|
|
220
|
+
# number). CONVENTIONS L51 requires it. Ships as WARN; flips to hard in Phase 3.
|
|
221
|
+
check_back_citation() {
|
|
222
|
+
local f tgt a bfile cand
|
|
223
|
+
while IFS= read -r f; do
|
|
224
|
+
tgt="$(adr_supersede_target "$f")"
|
|
225
|
+
printf '%s' "$tgt" | grep -qE '^[0-9]{4}$' || continue
|
|
226
|
+
a="$(adr_num "$f")"
|
|
227
|
+
# Resolve the successor file via the same defensive-glob idiom as adr_files,
|
|
228
|
+
# NOT `ls ... | head` in a command substitution: under set -e + pipefail a
|
|
229
|
+
# missing target would make ls fail and abort the whole script before later
|
|
230
|
+
# checks and the summary run.
|
|
231
|
+
bfile=""
|
|
232
|
+
for cand in "$ADR_DIR/${tgt}-"*.md; do
|
|
233
|
+
[ -e "$cand" ] && { bfile="$cand"; break; }
|
|
234
|
+
done
|
|
235
|
+
[ -n "$bfile" ] || continue # dangling target already caught by the supersede-target check
|
|
236
|
+
if grep -qE "(^|[^0-9])${a}([^0-9]|$)" "$bfile"; then
|
|
237
|
+
continue
|
|
238
|
+
fi
|
|
239
|
+
# Fail safe: only an explicit "warn" downgrades; any other value (including a
|
|
240
|
+
# typo'd BACKCITE_MODE) stays hard so enforcement is never silently lost.
|
|
241
|
+
if [ "$BACKCITE_MODE" = warn ]; then
|
|
242
|
+
echo " [WARN] $bfile — supersedes $a but never cites it (CONVENTIONS L51); backfill required"
|
|
243
|
+
WARNINGS=$((WARNINGS + 1))
|
|
244
|
+
else
|
|
245
|
+
echo " [VIOLATION] $bfile — supersedes $a but never cites it (CONVENTIONS L51)"
|
|
246
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
247
|
+
fi
|
|
248
|
+
done < <(adr_files)
|
|
249
|
+
}
|
|
250
|
+
check_back_citation
|
|
251
|
+
|
|
252
|
+
# Impl-drift hint: an Accepted ADR with no **Implementation** pointer may have
|
|
253
|
+
# rotted or never shipped. WARN only — without a machine-readable **Code** glob
|
|
254
|
+
# (deferred) we can only hint, and a hard gate on a heuristic trains people to
|
|
255
|
+
# ignore the linter.
|
|
256
|
+
check_impl_drift() {
|
|
257
|
+
local f s impl
|
|
258
|
+
while IFS= read -r f; do
|
|
259
|
+
s="$(adr_field "$f" Status)"
|
|
260
|
+
[ "$s" = Accepted ] || continue
|
|
261
|
+
impl="$(adr_field "$f" Implementation)"
|
|
262
|
+
if [ -z "$impl" ]; then
|
|
263
|
+
echo " [WARN] $f — Accepted but no **Implementation** pointer (possible drift)"
|
|
264
|
+
WARNINGS=$((WARNINGS + 1))
|
|
265
|
+
fi
|
|
266
|
+
done < <(adr_files)
|
|
267
|
+
}
|
|
268
|
+
check_impl_drift
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if [ $VIOLATIONS -eq 0 ]; then
|
|
272
|
+
if [ $GRANDFATHERED_HITS -gt 0 ]; then
|
|
273
|
+
echo " [OK] no new violations (${GRANDFATHERED_HITS} grandfathered awaiting cleanup)"
|
|
274
|
+
else
|
|
275
|
+
echo " [OK] all ADRs in terminal state"
|
|
276
|
+
fi
|
|
277
|
+
[ $WARNINGS -gt 0 ] && echo " [NOTE] ${WARNINGS} warning(s) — see WARN lines above (non-blocking)"
|
|
278
|
+
exit 0
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
echo
|
|
282
|
+
echo " ${VIOLATIONS} new violation(s). See docs/adr/CONVENTIONS.md."
|
|
283
|
+
exit 1
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check-comment-i18n: fail if CJK appears in genuine code comments.
|
|
3
|
+
|
|
4
|
+
A COMMENT gate, not a whole-repo CJK gate. CJK inside string literals, raw
|
|
5
|
+
strings (Go backticks), rune/char literals, URLs, or thrift annotation values
|
|
6
|
+
is RUNTIME DATA and is explicitly OUT of scope — it must not be flagged.
|
|
7
|
+
|
|
8
|
+
Inputs: newline-separated file paths on stdin (already filtered/excluded by
|
|
9
|
+
the bash wrapper). Output: `path:line: <text>` per hit; exit 1 on any hit
|
|
10
|
+
(plus a stderr summary), exit 0 otherwise.
|
|
11
|
+
|
|
12
|
+
Pure python3 stdlib — no third-party imports.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
# Ideographs (+Ext-A), kana, hangul, CJK/full-width punctuation.
|
|
20
|
+
# Same ranges as the historical text-based gate.
|
|
21
|
+
CJK = re.compile('[ -〿-ヿ㐀-䶿'
|
|
22
|
+
'一-鿿가--]')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_or_fail(path):
|
|
26
|
+
"""Read a file as UTF-8, failing CLOSED on any OSError.
|
|
27
|
+
|
|
28
|
+
This is a gate: "cannot verify == do not pass". A swallowed read error
|
|
29
|
+
would let a file with CJK comments slip through silently, so emit a clear
|
|
30
|
+
stderr signal and exit non-zero instead of skipping the file.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
with open(path, encoding='utf-8', errors='replace') as f:
|
|
34
|
+
return f.read()
|
|
35
|
+
except OSError as e:
|
|
36
|
+
print(f'check-comment-i18n: ERROR: cannot read {path}: {e}',
|
|
37
|
+
file=sys.stderr)
|
|
38
|
+
sys.exit(2)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def comment_hits(path, *, hash_line_comment, backtick_rawstring):
|
|
42
|
+
"""Character-level state machine; return [(line, text)] for CJK comments.
|
|
43
|
+
|
|
44
|
+
States: code, line_comment, block_comment, string (``"`` or ``'``), and
|
|
45
|
+
(Go only) backtick_rawstring. Only text genuinely inside a comment is
|
|
46
|
+
checked for CJK. Block comments report the line where ``/*`` began.
|
|
47
|
+
"""
|
|
48
|
+
out = []
|
|
49
|
+
data = _read_or_fail(path)
|
|
50
|
+
|
|
51
|
+
state = 'code'
|
|
52
|
+
i = 0
|
|
53
|
+
n = len(data)
|
|
54
|
+
line = 1
|
|
55
|
+
buf = [] # collected comment text
|
|
56
|
+
start_line = 0 # line where the current comment started
|
|
57
|
+
prefix = '' # opener of the current line comment ('//' or '#')
|
|
58
|
+
string_end = '' # closing delimiter of the open string literal
|
|
59
|
+
|
|
60
|
+
while i < n:
|
|
61
|
+
c = data[i]
|
|
62
|
+
nxt = data[i + 1] if i + 1 < n else ''
|
|
63
|
+
|
|
64
|
+
if state == 'code':
|
|
65
|
+
if c == '"' or c == "'":
|
|
66
|
+
state = 'string'
|
|
67
|
+
string_end = c
|
|
68
|
+
elif backtick_rawstring and c == '`':
|
|
69
|
+
state = 'backtick_rawstring'
|
|
70
|
+
elif c == '/' and nxt == '/':
|
|
71
|
+
state = 'line_comment'
|
|
72
|
+
buf = []
|
|
73
|
+
prefix = '//'
|
|
74
|
+
start_line = line
|
|
75
|
+
i += 2
|
|
76
|
+
continue
|
|
77
|
+
elif c == '/' and nxt == '*':
|
|
78
|
+
state = 'block_comment'
|
|
79
|
+
buf = []
|
|
80
|
+
start_line = line
|
|
81
|
+
i += 2
|
|
82
|
+
continue
|
|
83
|
+
elif hash_line_comment and c == '#':
|
|
84
|
+
state = 'line_comment'
|
|
85
|
+
buf = []
|
|
86
|
+
prefix = '#'
|
|
87
|
+
start_line = line
|
|
88
|
+
# any other char (incl. a lone '/') stays in code
|
|
89
|
+
|
|
90
|
+
elif state == 'string':
|
|
91
|
+
if c == '\\':
|
|
92
|
+
i += 2
|
|
93
|
+
# a backslash-newline still advances the line counter
|
|
94
|
+
if i - 1 < n and data[i - 1] == '\n':
|
|
95
|
+
line += 1
|
|
96
|
+
continue
|
|
97
|
+
elif c == string_end:
|
|
98
|
+
state = 'code'
|
|
99
|
+
|
|
100
|
+
elif state == 'backtick_rawstring':
|
|
101
|
+
# No escapes; spans newlines; only a closing backtick ends it.
|
|
102
|
+
if c == '`':
|
|
103
|
+
state = 'code'
|
|
104
|
+
|
|
105
|
+
elif state == 'line_comment':
|
|
106
|
+
if c == '\n':
|
|
107
|
+
text = ''.join(buf)
|
|
108
|
+
if CJK.search(text):
|
|
109
|
+
out.append((start_line, (prefix + text).rstrip()))
|
|
110
|
+
state = 'code'
|
|
111
|
+
else:
|
|
112
|
+
buf.append(c)
|
|
113
|
+
|
|
114
|
+
elif state == 'block_comment':
|
|
115
|
+
if c == '*' and nxt == '/':
|
|
116
|
+
text = ''.join(buf)
|
|
117
|
+
if CJK.search(text):
|
|
118
|
+
first = text.split('\n', 1)[0]
|
|
119
|
+
out.append((start_line, ('/*' + first).rstrip()))
|
|
120
|
+
state = 'code'
|
|
121
|
+
i += 2
|
|
122
|
+
continue
|
|
123
|
+
else:
|
|
124
|
+
buf.append(c)
|
|
125
|
+
|
|
126
|
+
if c == '\n':
|
|
127
|
+
line += 1
|
|
128
|
+
i += 1
|
|
129
|
+
|
|
130
|
+
# Unterminated trailing comment (EOF without newline / closing */).
|
|
131
|
+
if state == 'line_comment':
|
|
132
|
+
text = ''.join(buf)
|
|
133
|
+
if CJK.search(text):
|
|
134
|
+
out.append((start_line, (prefix + text).rstrip()))
|
|
135
|
+
elif state == 'block_comment':
|
|
136
|
+
text = ''.join(buf)
|
|
137
|
+
if CJK.search(text):
|
|
138
|
+
first = text.split('\n', 1)[0]
|
|
139
|
+
out.append((start_line, ('/*' + first).rstrip()))
|
|
140
|
+
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _json_line_for(raw, value):
|
|
145
|
+
"""Best-effort: 1-based line of a "Comment" value containing CJK.
|
|
146
|
+
|
|
147
|
+
Scan raw text for a "Comment" key whose value substring matches. Falls
|
|
148
|
+
back to scanning for the value substring alone, else 1.
|
|
149
|
+
"""
|
|
150
|
+
lines = raw.splitlines()
|
|
151
|
+
for idx, ln in enumerate(lines, 1):
|
|
152
|
+
if '"Comment"' in ln and value in ln:
|
|
153
|
+
return idx
|
|
154
|
+
for idx, ln in enumerate(lines, 1):
|
|
155
|
+
if value in ln:
|
|
156
|
+
return idx
|
|
157
|
+
return 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def json_hits(path):
|
|
161
|
+
"""Find CJK in JSON ``Comment`` string values; report real line numbers."""
|
|
162
|
+
out = []
|
|
163
|
+
raw = _read_or_fail(path)
|
|
164
|
+
try:
|
|
165
|
+
data = json.loads(raw)
|
|
166
|
+
except ValueError:
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
def walk(o):
|
|
170
|
+
if isinstance(o, dict):
|
|
171
|
+
for k, v in o.items():
|
|
172
|
+
if k == 'Comment' and isinstance(v, str) and CJK.search(v):
|
|
173
|
+
out.append((_json_line_for(raw, v), f'Comment: {v}'))
|
|
174
|
+
walk(v)
|
|
175
|
+
elif isinstance(o, list):
|
|
176
|
+
for x in o:
|
|
177
|
+
walk(x)
|
|
178
|
+
|
|
179
|
+
walk(data)
|
|
180
|
+
return out
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def hits_for(path):
|
|
184
|
+
if path.endswith('.go'):
|
|
185
|
+
return comment_hits(path, hash_line_comment=False,
|
|
186
|
+
backtick_rawstring=True)
|
|
187
|
+
if path.endswith('.thrift'):
|
|
188
|
+
return comment_hits(path, hash_line_comment=True,
|
|
189
|
+
backtick_rawstring=False)
|
|
190
|
+
if path.endswith('.json'):
|
|
191
|
+
return json_hits(path)
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main():
|
|
196
|
+
bad = 0
|
|
197
|
+
for path in (p.strip() for p in sys.stdin if p.strip()):
|
|
198
|
+
for n, text in sorted(set(hits_for(path))):
|
|
199
|
+
bad += 1
|
|
200
|
+
print(f'{path}:{n}: {text}')
|
|
201
|
+
|
|
202
|
+
if bad:
|
|
203
|
+
print(f'\n[check-comment-i18n] {bad} CJK comment(s) found — '
|
|
204
|
+
f'comments must be English.', file=sys.stderr)
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == '__main__':
|
|
209
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-comment-i18n.sh — fail if CJK appears in Go code comments. A COMMENT
|
|
3
|
+
# gate for Go source only: runtime strings / prompts are out of scope, and so
|
|
4
|
+
# are the external-facing API contracts (IDL .thrift comments, OpenAPI, and
|
|
5
|
+
# errno JSON Comment fields) which are intentionally kept in Chinese for
|
|
6
|
+
# external integrators. Tokenization lives in the committed
|
|
7
|
+
# check-comment-i18n.py; this wrapper only does file discovery + exclusion.
|
|
8
|
+
#
|
|
9
|
+
# Usage: check-comment-i18n.sh [PATH...] (default: .)
|
|
10
|
+
# - A PATH that is a directory is scanned recursively for *.go files.
|
|
11
|
+
# - A PATH that is a FILE is included iff it is a *.go file. The same exclude
|
|
12
|
+
# regex applies.
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
|
|
16
|
+
# Default to whole tree when no path given.
|
|
17
|
+
if [ "$#" -eq 0 ]; then
|
|
18
|
+
set -- .
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
EXCLUDE='thrift_gen|\.pb\.go|_gen\.go|mock_|/mocks/|zz_generated|/vendor/|(^|/)\.claude/|(^|/)\.codex/'
|
|
22
|
+
|
|
23
|
+
discover() {
|
|
24
|
+
local target
|
|
25
|
+
for target in "$@"; do
|
|
26
|
+
if [ -d "$target" ]; then
|
|
27
|
+
find "$target" -type f -name '*.go' 2>/dev/null
|
|
28
|
+
elif [ -f "$target" ]; then
|
|
29
|
+
case "$target" in
|
|
30
|
+
*.go) printf '%s\n' "$target" ;;
|
|
31
|
+
esac
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
discover "$@" \
|
|
37
|
+
| { grep -vE "$EXCLUDE" || true; } \
|
|
38
|
+
| python3 "$HERE/check-comment-i18n.py"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Verify that repo paths cited in component docs (docs/NN-*.md) exist on disk.
|
|
3
|
+
#
|
|
4
|
+
# This complements check-docs.sh (which only lints filename/title/numbering
|
|
5
|
+
# structure) with a lightweight content<->code guard: it is the cheapest way to
|
|
6
|
+
# catch the "doc names a file/dir that was renamed or deleted" drift class that
|
|
7
|
+
# previously slipped through CI.
|
|
8
|
+
#
|
|
9
|
+
# Scope / non-goals:
|
|
10
|
+
# - Only docs/NN-*.md (component & architecture docs). ADRs are immutable
|
|
11
|
+
# historical records and are intentionally NOT scanned.
|
|
12
|
+
# - configs/ is out of scope (runtime artifacts; owned by coding-deploy-config).
|
|
13
|
+
# - Removal / placeholder / 规划中 lines are ignored on purpose.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
cd "$(dirname "$0")/.."
|
|
16
|
+
|
|
17
|
+
# shellcheck source=scripts/lib/doc-drift.sh
|
|
18
|
+
. scripts/lib/doc-drift.sh
|
|
19
|
+
|
|
20
|
+
echo "==> Checking repo path references in docs/NN-*.md resolve on disk..."
|
|
21
|
+
|
|
22
|
+
broken=0
|
|
23
|
+
for f in docs/[0-9]*-*.md; do
|
|
24
|
+
[[ -f "$f" ]] || continue
|
|
25
|
+
while IFS= read -r hit; do
|
|
26
|
+
[[ -z "$hit" ]] && continue
|
|
27
|
+
echo " [FAIL] doc references missing path -> ${hit}"
|
|
28
|
+
broken=$((broken + 1))
|
|
29
|
+
done < <(doc_drift_check_paths "$f")
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [[ "$broken" -gt 0 ]]; then
|
|
33
|
+
echo
|
|
34
|
+
echo " ${broken} broken path reference(s). Fix the path in the doc, or — if the"
|
|
35
|
+
echo " path is intentionally absent — phrase it as a removal/placeholder/规划中,"
|
|
36
|
+
echo " which this check skips. configs/ paths are out of scope."
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo " [OK] all repo path references resolve"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Validate filename pattern, title-number alignment, and number uniqueness for:
|
|
3
|
+
# docs/NN-*.md (component/architecture, 2-digit prefix)
|
|
4
|
+
# docs/adr/NNNN-*.md (architecture decisions, 4-digit prefix)
|
|
5
|
+
#
|
|
6
|
+
# Status field (Accepted/Deprecated/...) is enforced by scripts/check-adr.sh.
|
|
7
|
+
# See docs/adr/CONVENTIONS.md.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
cd "$(dirname "$0")/.."
|
|
11
|
+
|
|
12
|
+
# Grandfathered violations — pending cleanup tracked in CONVENTIONS.md.
|
|
13
|
+
# Removing an entry here MUST coincide with cleaning up CONVENTIONS.md table
|
|
14
|
+
# AND scripts/check-adr.sh GRANDFATHERED if applicable.
|
|
15
|
+
GRANDFATHERED=()
|
|
16
|
+
|
|
17
|
+
is_grandfathered() {
|
|
18
|
+
local f=$1
|
|
19
|
+
for g in "${GRANDFATHERED[@]+"${GRANDFATHERED[@]}"}"; do
|
|
20
|
+
[ "$f" = "$g" ] && return 0
|
|
21
|
+
done
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
VIOLATIONS=0
|
|
26
|
+
GRANDFATHERED_HITS=0
|
|
27
|
+
|
|
28
|
+
# check_dir <directory> <digit-count>
|
|
29
|
+
check_dir() {
|
|
30
|
+
local dir=$1 digits=$2
|
|
31
|
+
local seen_nums=""
|
|
32
|
+
local fname_pattern="^[0-9]{${digits}}-[a-z0-9-]+\.md$"
|
|
33
|
+
local title_pattern="^#[[:space:]]+([0-9]+)[[:space:]]+-[[:space:]]+.+$"
|
|
34
|
+
|
|
35
|
+
for f in "$dir"/[0-9]*.md; do
|
|
36
|
+
[ -f "$f" ] || continue
|
|
37
|
+
local base num title title_num grandfather_msg
|
|
38
|
+
base=$(basename "$f")
|
|
39
|
+
grandfather_msg=""
|
|
40
|
+
is_grandfathered "$f" && grandfather_msg=" (cleanup tracked in CONVENTIONS.md)"
|
|
41
|
+
|
|
42
|
+
# 1. Filename pattern
|
|
43
|
+
if ! [[ "$base" =~ $fname_pattern ]]; then
|
|
44
|
+
if is_grandfathered "$f"; then
|
|
45
|
+
echo " [GRANDFATHERED] $f — filename does not match ^[0-9]{${digits}}-[a-z0-9-]+\\.md\$$grandfather_msg"
|
|
46
|
+
GRANDFATHERED_HITS=$((GRANDFATHERED_HITS + 1))
|
|
47
|
+
else
|
|
48
|
+
echo " [VIOLATION] $f — filename does not match ^[0-9]{${digits}}-[a-z0-9-]+\\.md\$"
|
|
49
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
50
|
+
fi
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
num=${base:0:$digits}
|
|
55
|
+
|
|
56
|
+
# 2. Number uniqueness within directory
|
|
57
|
+
if echo "$seen_nums" | grep -qx "$num"; then
|
|
58
|
+
echo " [VIOLATION] $f — number $num already used in $dir"
|
|
59
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
60
|
+
else
|
|
61
|
+
seen_nums="$seen_nums"$'\n'"$num"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 3. Title pattern + number alignment
|
|
65
|
+
title=$(head -1 "$f")
|
|
66
|
+
if ! [[ "$title" =~ $title_pattern ]]; then
|
|
67
|
+
if is_grandfathered "$f"; then
|
|
68
|
+
echo " [GRANDFATHERED] $f — title does not match '# NUM - ...'$grandfather_msg"
|
|
69
|
+
GRANDFATHERED_HITS=$((GRANDFATHERED_HITS + 1))
|
|
70
|
+
else
|
|
71
|
+
echo " [VIOLATION] $f — title '$title' does not match '# NUM - ...'"
|
|
72
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
73
|
+
fi
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
76
|
+
title_num=${BASH_REMATCH[1]}
|
|
77
|
+
if [ "$title_num" != "$num" ]; then
|
|
78
|
+
if is_grandfathered "$f"; then
|
|
79
|
+
echo " [GRANDFATHERED] $f — title number '$title_num' ≠ filename '$num'$grandfather_msg"
|
|
80
|
+
GRANDFATHERED_HITS=$((GRANDFATHERED_HITS + 1))
|
|
81
|
+
else
|
|
82
|
+
echo " [VIOLATION] $f — title number '$title_num' does not match filename '$num'"
|
|
83
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
done
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
check_dir docs 2
|
|
90
|
+
check_dir docs/adr 4
|
|
91
|
+
|
|
92
|
+
if [ $VIOLATIONS -eq 0 ]; then
|
|
93
|
+
if [ $GRANDFATHERED_HITS -gt 0 ]; then
|
|
94
|
+
echo " [OK] no new violations (${GRANDFATHERED_HITS} grandfathered awaiting cleanup)"
|
|
95
|
+
else
|
|
96
|
+
echo " [OK] filename / title / numbering all consistent"
|
|
97
|
+
fi
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
echo
|
|
102
|
+
echo " ${VIOLATIONS} new violation(s). See docs/adr/CONVENTIONS.md."
|
|
103
|
+
exit 1
|