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,243 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Skill governance hard checks for every skill under .agents/skills/coding-*.
|
|
3
|
+
#
|
|
4
|
+
# Hard checks (FAIL on violation):
|
|
5
|
+
# 1. SKILL.md exists
|
|
6
|
+
# 2. Frontmatter contains: name, description
|
|
7
|
+
# 3. Frontmatter `name` matches the directory name
|
|
8
|
+
# 4. Internal refs — `.agents/skills/<name>/(patterns|templates)/<file>` exist
|
|
9
|
+
# 5. Drift detection — repo-rooted paths cited in SKILL.md exist on disk
|
|
10
|
+
# (literal paths starting with internal/, docs/, migrations/, api/, etc.)
|
|
11
|
+
# 6. coding-testing Layer→Strategy table covers every outbound adapter package
|
|
12
|
+
# 7. Application code does not silently replace missing injected loggers with logger.NewNop()
|
|
13
|
+
#
|
|
14
|
+
# Soft checks (WARN, no FAIL):
|
|
15
|
+
# 8. Skill cross-reference graph drift
|
|
16
|
+
#
|
|
17
|
+
# This script intentionally keeps semantic checks small and repo-specific.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
cd "$(dirname "$0")/.."
|
|
22
|
+
|
|
23
|
+
# shellcheck source=scripts/lib/skill-frontmatter.sh
|
|
24
|
+
. scripts/lib/skill-frontmatter.sh
|
|
25
|
+
# shellcheck source=scripts/lib/doc-drift.sh
|
|
26
|
+
. scripts/lib/doc-drift.sh
|
|
27
|
+
|
|
28
|
+
GLOBAL_VIOLATIONS=0
|
|
29
|
+
|
|
30
|
+
# Materialise the set of real *.go basenames once for the bare-filename drift
|
|
31
|
+
# check (── 5b ──). Tables that cite a single `foo.go` whose basename exists
|
|
32
|
+
# nowhere in the repo are flagged.
|
|
33
|
+
DOC_DRIFT_GO_BASENAMES="$(mktemp)"
|
|
34
|
+
trap 'rm -f "$DOC_DRIFT_GO_BASENAMES"' EXIT
|
|
35
|
+
doc_drift_go_basenames_to "$DOC_DRIFT_GO_BASENAMES"
|
|
36
|
+
|
|
37
|
+
# Repo-rooted prefixes for drift detection. Anything matching one of these
|
|
38
|
+
# at the start of a token is treated as a real path that must exist on disk.
|
|
39
|
+
DRIFT_PREFIXES='^(internal|docs|migrations|api|configs|deployments|scripts|cmd|pkg|tests|tools|web|gen)/'
|
|
40
|
+
|
|
41
|
+
check_skill() {
|
|
42
|
+
local dir=$1
|
|
43
|
+
local name
|
|
44
|
+
name=$(basename "$dir")
|
|
45
|
+
local skill_md="$dir/SKILL.md"
|
|
46
|
+
local violations=0
|
|
47
|
+
|
|
48
|
+
echo "── $name ─────────────────────────────────────"
|
|
49
|
+
|
|
50
|
+
if [[ ! -f "$skill_md" ]]; then
|
|
51
|
+
echo " [FAIL] $skill_md missing"
|
|
52
|
+
GLOBAL_VIOLATIONS=$((GLOBAL_VIOLATIONS+1))
|
|
53
|
+
return
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ── 1+2+3. Frontmatter sanity ─────────────────────────────────────────
|
|
57
|
+
for field in name description; do
|
|
58
|
+
local val
|
|
59
|
+
val=$(extract_field "$skill_md" "$field")
|
|
60
|
+
if [[ -z "$val" ]]; then
|
|
61
|
+
echo " [FAIL] frontmatter missing '$field'"
|
|
62
|
+
violations=$((violations+1))
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
local declared_name
|
|
67
|
+
declared_name=$(extract_field "$skill_md" "name")
|
|
68
|
+
if [[ -n "$declared_name" ]] && [[ "$declared_name" != "$name" ]]; then
|
|
69
|
+
echo " [FAIL] frontmatter name='$declared_name' but directory is '$name'"
|
|
70
|
+
violations=$((violations+1))
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# ── 4. Internal refs ──────────────────────────────────────────────────
|
|
74
|
+
local internal_pattern="\.agents/skills/${name}/(patterns|templates)/[a-zA-Z0-9._-]+"
|
|
75
|
+
local internal_refs
|
|
76
|
+
internal_refs=$(grep -oE "$internal_pattern" "$skill_md" | sort -u || true)
|
|
77
|
+
local internal_count=0
|
|
78
|
+
if [[ -n "$internal_refs" ]]; then
|
|
79
|
+
while IFS= read -r ref; do
|
|
80
|
+
[[ -z "$ref" ]] && continue
|
|
81
|
+
internal_count=$((internal_count+1))
|
|
82
|
+
if [[ ! -f "$ref" ]]; then
|
|
83
|
+
echo " [FAIL] internal ref missing: $ref"
|
|
84
|
+
violations=$((violations+1))
|
|
85
|
+
fi
|
|
86
|
+
done <<< "$internal_refs"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# ── 5. Drift detection — external repo paths ────────────────────────
|
|
90
|
+
# Extract candidate paths: tokens that look like a real file/dir
|
|
91
|
+
# rooted at one of the known repo prefixes. We trim trailing :NN line
|
|
92
|
+
# numbers and trailing punctuation, and skip placeholder paths
|
|
93
|
+
# containing < > * { or ellipsis-style segments.
|
|
94
|
+
local drift_count=0
|
|
95
|
+
local drift_broken=0
|
|
96
|
+
while IFS= read -r raw; do
|
|
97
|
+
[[ -z "$raw" ]] && continue
|
|
98
|
+
|
|
99
|
+
# Strip trailing line ref like ":123"
|
|
100
|
+
local p="${raw%%:[0-9]*}"
|
|
101
|
+
# Strip trailing punctuation common in markdown prose
|
|
102
|
+
p="${p%[\.\,\)\;\]\`]}"
|
|
103
|
+
|
|
104
|
+
# Skip placeholders / globs / shell expansion patterns
|
|
105
|
+
case "$p" in
|
|
106
|
+
*\<* | *\>* | *\** | *\{* | *\$* )
|
|
107
|
+
continue ;;
|
|
108
|
+
esac
|
|
109
|
+
|
|
110
|
+
# Skip tokens ending in connective chars — these are typically
|
|
111
|
+
# captures truncated at a `<placeholder>` boundary, e.g.
|
|
112
|
+
# `deployments/coding-<svc>/...` → grep -oE stops at `<` and yields
|
|
113
|
+
# `deployments/coding-`. Real paths in this repo never end in
|
|
114
|
+
# `-`, `_`, `=`, or a bare `.`.
|
|
115
|
+
case "$p" in
|
|
116
|
+
*-|*_|*=|*.) continue ;;
|
|
117
|
+
esac
|
|
118
|
+
|
|
119
|
+
# Skip if any path segment is an all-caps placeholder run
|
|
120
|
+
# (`NNNNNN`, `XX`, `0N`) — these are doc conventions, not real names
|
|
121
|
+
if [[ "$p" =~ (^|/)(N{2,}|X{2,}|0N)([_.-]|$|/) ]]; then
|
|
122
|
+
continue
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Re-check the trimmed token still starts with a known prefix
|
|
126
|
+
# (the regex below is the one used to match in the first place,
|
|
127
|
+
# but `${var%...}` may have stripped past the prefix in pathological
|
|
128
|
+
# cases — be defensive)
|
|
129
|
+
if ! [[ "$p" =~ $DRIFT_PREFIXES ]]; then
|
|
130
|
+
continue
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
drift_count=$((drift_count+1))
|
|
134
|
+
|
|
135
|
+
# `-e` matches both files and directories — covers the
|
|
136
|
+
# "internal/foo/" trailing-slash case the extractor produces.
|
|
137
|
+
if [[ -e "$p" ]]; then
|
|
138
|
+
continue
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo " [FAIL] drift — referenced path missing: $p"
|
|
142
|
+
violations=$((violations+1))
|
|
143
|
+
drift_broken=$((drift_broken+1))
|
|
144
|
+
done < <(grep -oE '[A-Za-z0-9_./-]+' "$skill_md" | grep -E "$DRIFT_PREFIXES" | sort -u || true)
|
|
145
|
+
|
|
146
|
+
# ── 5b. Bare *.go filenames in tables ─────────────────────────────────
|
|
147
|
+
# The §5 drift check above only matches repo-rooted paths (internal/…).
|
|
148
|
+
# File-inventory tables, however, cite bare basenames like `volcid.go` in
|
|
149
|
+
# the first column — invisible to §5. Flag any such cell whose basename
|
|
150
|
+
# exists nowhere in the repo (deleted/renamed file still documented).
|
|
151
|
+
while IFS= read -r hit; do
|
|
152
|
+
[[ -z "$hit" ]] && continue
|
|
153
|
+
echo " [FAIL] drift — table cites non-existent Go file: ${hit##*: }"
|
|
154
|
+
violations=$((violations+1))
|
|
155
|
+
drift_broken=$((drift_broken+1))
|
|
156
|
+
done < <(doc_drift_check_table_go "$skill_md")
|
|
157
|
+
|
|
158
|
+
# ── Summary line per skill ───────────────────────────────────────────
|
|
159
|
+
if [[ "$violations" -eq 0 ]]; then
|
|
160
|
+
local drift_summary=""
|
|
161
|
+
if [[ "$drift_count" -gt 0 ]]; then
|
|
162
|
+
drift_summary=", $drift_count drift refs resolved"
|
|
163
|
+
fi
|
|
164
|
+
echo " [OK] frontmatter valid, ${internal_count} internal refs resolved${drift_summary}"
|
|
165
|
+
else
|
|
166
|
+
GLOBAL_VIOLATIONS=$((GLOBAL_VIOLATIONS+violations))
|
|
167
|
+
fi
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if [[ ! -d "$SKILLS_ROOT" ]]; then
|
|
171
|
+
echo "[ERROR] $SKILLS_ROOT not found" >&2
|
|
172
|
+
exit 1
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# ── Application logger injection guard ───────────────────────────────────────
|
|
176
|
+
# Business/application constructors must fail fast when a logger dependency is
|
|
177
|
+
# missing. Tests can pass logger.NewNop() explicitly, but production code must
|
|
178
|
+
# not hide a DI wiring bug by silently creating one.
|
|
179
|
+
echo "── application-logger-injection ─────────────────────────────────────"
|
|
180
|
+
LOGGER_FALLBACK_HITS=$(
|
|
181
|
+
grep -RIn --include='*.go' 'logger\.NewNop()' internal/application 2>/dev/null \
|
|
182
|
+
| grep -v '_test.go' || true
|
|
183
|
+
)
|
|
184
|
+
if [[ -n "$LOGGER_FALLBACK_HITS" ]]; then
|
|
185
|
+
echo "$LOGGER_FALLBACK_HITS" | sed 's/^/ [FAIL] /'
|
|
186
|
+
echo " [HINT] Inject logger.Logger via deps/fx; tests may pass logger.NewNop() explicitly."
|
|
187
|
+
GLOBAL_VIOLATIONS=$((GLOBAL_VIOLATIONS+1))
|
|
188
|
+
else
|
|
189
|
+
echo " [OK] no logger.NewNop() fallback in internal/application non-test code"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# ── coding-testing Layer→Strategy coverage ───────────────────────────────────
|
|
193
|
+
# Every outbound adapter package needs an explicit testing strategy entry so
|
|
194
|
+
# new adapters do not silently skip the test ownership rule.
|
|
195
|
+
echo "── coding-testing-layer-strategy ─────────────────────────────────────"
|
|
196
|
+
TESTING_SKILL="$SKILLS_ROOT/coding-testing/SKILL.md"
|
|
197
|
+
if [[ ! -f "$TESTING_SKILL" ]]; then
|
|
198
|
+
echo " [FAIL] $TESTING_SKILL missing"
|
|
199
|
+
GLOBAL_VIOLATIONS=$((GLOBAL_VIOLATIONS+1))
|
|
200
|
+
else
|
|
201
|
+
HAS_MISSING=0
|
|
202
|
+
for dir in internal/adapters/outbound/*; do
|
|
203
|
+
[[ -d "$dir" ]] || continue
|
|
204
|
+
pkg=$(basename "$dir")
|
|
205
|
+
if ! grep -q "adapters/outbound/$pkg" "$TESTING_SKILL"; then
|
|
206
|
+
echo " [FAIL] adapters/outbound/$pkg has no Layer→Strategy table entry"
|
|
207
|
+
HAS_MISSING=1
|
|
208
|
+
GLOBAL_VIOLATIONS=$((GLOBAL_VIOLATIONS+1))
|
|
209
|
+
fi
|
|
210
|
+
done
|
|
211
|
+
if [[ "$HAS_MISSING" -eq 0 ]]; then
|
|
212
|
+
echo " [OK] all outbound adapter packages covered"
|
|
213
|
+
fi
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
echo ""
|
|
217
|
+
|
|
218
|
+
for dir in "$SKILLS_ROOT"/coding-*; do
|
|
219
|
+
[[ -d "$dir" ]] || continue
|
|
220
|
+
check_skill "$dir"
|
|
221
|
+
done
|
|
222
|
+
|
|
223
|
+
# ── Cross-reference graph drift (soft warning) ───────────────────────────────
|
|
224
|
+
# Almost-free: regenerate the DOT in-memory and compare to the committed copy.
|
|
225
|
+
# Drift here is informational, not a hard failure (the graph is a derived
|
|
226
|
+
# artifact; tests / functionality don't depend on it being current).
|
|
227
|
+
if [[ -x scripts/skill-graph.sh ]]; then
|
|
228
|
+
echo ""
|
|
229
|
+
echo "── skill-graph ─────────────────────────────────────"
|
|
230
|
+
if bash scripts/skill-graph.sh --check >/dev/null 2>&1; then
|
|
231
|
+
echo " [OK] docs/assets/skill-graph.dot matches current cross-references"
|
|
232
|
+
else
|
|
233
|
+
echo " [STALE] docs/assets/skill-graph.dot is out of sync with SKILL.md cross-references"
|
|
234
|
+
echo " run \`make skill-graph\` to regenerate, then commit the diff"
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
echo ""
|
|
239
|
+
if [[ "$GLOBAL_VIOLATIONS" -gt 0 ]]; then
|
|
240
|
+
echo "[FAIL] $GLOBAL_VIOLATIONS violation(s)"
|
|
241
|
+
exit 1
|
|
242
|
+
fi
|
|
243
|
+
echo "[OK] all skills pass structural integrity check"
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""File docs-audit implementation follow-ups as Codebase issues."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shlex
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_REPORT = "docs-audit-notification-report.md"
|
|
20
|
+
DEFAULT_SUMMARY = "docs-audit-code-issues.md"
|
|
21
|
+
SEVERITY_RANK = {
|
|
22
|
+
"Suggestion": 0,
|
|
23
|
+
"Minor": 1,
|
|
24
|
+
"Major": 2,
|
|
25
|
+
"Critical": 3,
|
|
26
|
+
}
|
|
27
|
+
FINDING_RE = re.compile(r"(?m)^#{1,6} \[(Critical|Major|Minor|Suggestion)\]\s+(.+?)\s*$")
|
|
28
|
+
AFTER_FINDINGS_RE = re.compile(
|
|
29
|
+
r"(?m)^#{1,6} (按人员整理的任务|按责任人归总的任务清单|待确认问题|执行过的关键命令)\s*$"
|
|
30
|
+
)
|
|
31
|
+
PEOPLE_RE = re.compile(r"(?m)^#{1,6} (按人员整理的任务|按责任人归总的任务清单)\s*$")
|
|
32
|
+
NEXT_SECTION_RE = re.compile(r"(?m)^#{1,6} (待确认问题|执行过的关键命令)\s*$")
|
|
33
|
+
PERSON_HEADING_RE = re.compile(r"^#{1,6}\s+`?(.+?)`?\s*$")
|
|
34
|
+
TASK_RE = re.compile(r"^\s*[-*]\s+`?\[(Critical|Major|Minor|Suggestion)\]\s+(.+?)`?[::].*$")
|
|
35
|
+
EMAIL_RE = re.compile(r"[\w.+-]+@bytedance\.com", re.IGNORECASE)
|
|
36
|
+
PATH_RE = re.compile(r"`([^`]+?)(?::\d+)?`")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Finding:
|
|
41
|
+
severity: str
|
|
42
|
+
title: str
|
|
43
|
+
body: str
|
|
44
|
+
assignee_emails: set[str] = field(default_factory=set)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_args() -> argparse.Namespace:
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
description="Create Codebase issues for docs-audit findings left for implementation follow-up."
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument("--report", default=DEFAULT_REPORT)
|
|
52
|
+
parser.add_argument("--summary", default=DEFAULT_SUMMARY)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--min-severity",
|
|
55
|
+
default="Major",
|
|
56
|
+
choices=("Suggestion", "Minor", "Major", "Critical"),
|
|
57
|
+
help="minimum severity to file, default: Major",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--apply",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="create issues; without this, only print dry-run commands",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--actor",
|
|
66
|
+
default=os.environ.get("CODING_ISSUE_AUTODEV_ACTOR", "human"),
|
|
67
|
+
choices=("human", "agent"),
|
|
68
|
+
help="issue API mode passed to scripts/open-issue.sh (default: env CODING_ISSUE_AUTODEV_ACTOR or human)",
|
|
69
|
+
)
|
|
70
|
+
return parser.parse_args()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main() -> int:
|
|
74
|
+
args = parse_args()
|
|
75
|
+
report_path = Path(args.report)
|
|
76
|
+
summary_path = Path(args.summary)
|
|
77
|
+
if not report_path.is_file():
|
|
78
|
+
write_summary(summary_path, ["## 代码跟进 Issue", "", f"- 未提交:报告文件不存在 `{report_path}`。"])
|
|
79
|
+
print(f"error: report file not found: {report_path}", file=sys.stderr)
|
|
80
|
+
return 2
|
|
81
|
+
|
|
82
|
+
report = report_path.read_text(encoding="utf-8")
|
|
83
|
+
findings = [
|
|
84
|
+
f
|
|
85
|
+
for f in parse_findings(report)
|
|
86
|
+
if SEVERITY_RANK.get(f.severity, 0) >= SEVERITY_RANK[args.min_severity]
|
|
87
|
+
]
|
|
88
|
+
if not findings:
|
|
89
|
+
write_summary(
|
|
90
|
+
summary_path,
|
|
91
|
+
[
|
|
92
|
+
"## 代码跟进 Issue",
|
|
93
|
+
"",
|
|
94
|
+
f"- 未提交:`{report_path}` 中没有 {args.min_severity} 及以上需要代码/配置/实现跟进的差异点。",
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
print("[docs-audit-to-issues] no findings above severity gate", file=sys.stderr)
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
failures = 0
|
|
101
|
+
summary = ["## 代码跟进 Issue", ""]
|
|
102
|
+
for finding in findings:
|
|
103
|
+
assignee_ids: list[str] = []
|
|
104
|
+
unresolved: list[str] = []
|
|
105
|
+
for email in sorted(finding.assignee_emails):
|
|
106
|
+
user_id = resolve_user_id(email)
|
|
107
|
+
if user_id:
|
|
108
|
+
assignee_ids.append(user_id)
|
|
109
|
+
else:
|
|
110
|
+
unresolved.append(email)
|
|
111
|
+
|
|
112
|
+
issue_body = build_issue_body(finding, report_path)
|
|
113
|
+
dedup_key = "docs-audit:" + hashlib.sha1(
|
|
114
|
+
f"{finding.severity}|{finding.title}|{first_evidence_token(finding.body)}".encode("utf-8")
|
|
115
|
+
).hexdigest()[:16]
|
|
116
|
+
cmd = [
|
|
117
|
+
"scripts/open-issue.sh",
|
|
118
|
+
"--title",
|
|
119
|
+
f"[docs-audit] {finding.title}",
|
|
120
|
+
"--status",
|
|
121
|
+
"todo",
|
|
122
|
+
"--dedup-key",
|
|
123
|
+
dedup_key,
|
|
124
|
+
"--actor",
|
|
125
|
+
args.actor,
|
|
126
|
+
]
|
|
127
|
+
for assignee_id in assignee_ids:
|
|
128
|
+
cmd.extend(["--assignee", assignee_id])
|
|
129
|
+
|
|
130
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as fp:
|
|
131
|
+
fp.write(issue_body)
|
|
132
|
+
body_path = Path(fp.name)
|
|
133
|
+
try:
|
|
134
|
+
cmd.extend(["--body-file", str(body_path)])
|
|
135
|
+
if not args.apply:
|
|
136
|
+
cmd.append("--dry-run")
|
|
137
|
+
print(
|
|
138
|
+
f"[docs-audit-to-issues] {'creating' if args.apply else 'dry-run'} issue: "
|
|
139
|
+
f"[{finding.severity}] {finding.title}",
|
|
140
|
+
file=sys.stderr,
|
|
141
|
+
)
|
|
142
|
+
result = subprocess.run(cmd, text=True, capture_output=True, check=False)
|
|
143
|
+
finally:
|
|
144
|
+
body_path.unlink(missing_ok=True)
|
|
145
|
+
|
|
146
|
+
output = (result.stdout + "\n" + result.stderr).strip()
|
|
147
|
+
if result.returncode != 0:
|
|
148
|
+
failures += 1
|
|
149
|
+
log_issue_failure(
|
|
150
|
+
label=f"[{finding.severity}] {finding.title}",
|
|
151
|
+
dedup_key=dedup_key,
|
|
152
|
+
assignees=format_people(finding.assignee_emails, assignee_ids, unresolved),
|
|
153
|
+
cmd=cmd,
|
|
154
|
+
result=result,
|
|
155
|
+
)
|
|
156
|
+
summary.extend(
|
|
157
|
+
[
|
|
158
|
+
f"- 创建失败:`[{finding.severity}] {finding.title}`",
|
|
159
|
+
f" - dedup:`{dedup_key}`",
|
|
160
|
+
f" - 负责人:{format_people(finding.assignee_emails, assignee_ids, unresolved)}",
|
|
161
|
+
f" - 错误:{one_line(output)}",
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
issue_ref = parse_issue_ref(output)
|
|
167
|
+
verb = "已提交/复用" if args.apply else "dry-run"
|
|
168
|
+
summary.extend(
|
|
169
|
+
[
|
|
170
|
+
f"- {verb}:{issue_ref or f'`[{finding.severity}] {finding.title}`'}",
|
|
171
|
+
f" - 差异点:`[{finding.severity}] {finding.title}`",
|
|
172
|
+
f" - 负责人:{format_people(finding.assignee_emails, assignee_ids, unresolved)}",
|
|
173
|
+
f" - dedup:`{dedup_key}`",
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
write_summary(summary_path, summary)
|
|
178
|
+
if failures:
|
|
179
|
+
print(f"[docs-audit-to-issues] failed to create {failures} issue(s)", file=sys.stderr)
|
|
180
|
+
return 1
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def parse_findings(report: str) -> list[Finding]:
|
|
185
|
+
findings: list[Finding] = []
|
|
186
|
+
matches = list(FINDING_RE.finditer(report))
|
|
187
|
+
task_emails = parse_task_emails(report)
|
|
188
|
+
for idx, match in enumerate(matches):
|
|
189
|
+
severity = match.group(1)
|
|
190
|
+
title = match.group(2).strip()
|
|
191
|
+
start = match.start()
|
|
192
|
+
if idx + 1 < len(matches):
|
|
193
|
+
end = matches[idx + 1].start()
|
|
194
|
+
else:
|
|
195
|
+
next_section = AFTER_FINDINGS_RE.search(report, match.end())
|
|
196
|
+
end = next_section.start() if next_section else len(report)
|
|
197
|
+
body = report[start:end].strip()
|
|
198
|
+
emails = set(task_emails.get(normalize_title(title), set()))
|
|
199
|
+
emails.update(email.lower() for email in EMAIL_RE.findall(body))
|
|
200
|
+
findings.append(Finding(severity=severity, title=title, body=body, assignee_emails=emails))
|
|
201
|
+
return findings
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_task_emails(report: str) -> dict[str, set[str]]:
|
|
205
|
+
match = PEOPLE_RE.search(report)
|
|
206
|
+
if not match:
|
|
207
|
+
return {}
|
|
208
|
+
section = report[match.start() :]
|
|
209
|
+
next_match = NEXT_SECTION_RE.search(section)
|
|
210
|
+
if next_match:
|
|
211
|
+
section = section[: next_match.start()]
|
|
212
|
+
|
|
213
|
+
result: dict[str, set[str]] = {}
|
|
214
|
+
current_emails: set[str] = set()
|
|
215
|
+
for line in section.splitlines()[1:]:
|
|
216
|
+
heading = PERSON_HEADING_RE.match(line)
|
|
217
|
+
if heading:
|
|
218
|
+
current_emails = {email.lower() for email in EMAIL_RE.findall(heading.group(1))}
|
|
219
|
+
continue
|
|
220
|
+
task = TASK_RE.match(line)
|
|
221
|
+
if task and current_emails:
|
|
222
|
+
result.setdefault(normalize_title(task.group(2)), set()).update(current_emails)
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def normalize_title(title: str) -> str:
|
|
227
|
+
return re.sub(r"\s+", " ", title.strip()).strip("`::")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def resolve_user_id(email: str) -> str | None:
|
|
231
|
+
username = email.split("@", 1)[0]
|
|
232
|
+
try:
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
["codebase", "user", "view", username],
|
|
235
|
+
text=True,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
check=False,
|
|
238
|
+
)
|
|
239
|
+
except FileNotFoundError:
|
|
240
|
+
print(f"[docs-audit-to-issues] codebase CLI not found while resolving assignee: {email}", file=sys.stderr)
|
|
241
|
+
return None
|
|
242
|
+
if result.returncode != 0:
|
|
243
|
+
print(
|
|
244
|
+
f"[docs-audit-to-issues] failed to resolve assignee {email}: "
|
|
245
|
+
f"codebase user view {username} exited {result.returncode}: {one_line(result.stderr or result.stdout)}",
|
|
246
|
+
file=sys.stderr,
|
|
247
|
+
)
|
|
248
|
+
return None
|
|
249
|
+
try:
|
|
250
|
+
data = json.loads(result.stdout)
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
print(
|
|
253
|
+
f"[docs-audit-to-issues] failed to parse codebase user view output for {email}: "
|
|
254
|
+
f"{one_line(result.stdout)}",
|
|
255
|
+
file=sys.stderr,
|
|
256
|
+
)
|
|
257
|
+
return None
|
|
258
|
+
if data.get("Error") or data.get("Code"):
|
|
259
|
+
print(
|
|
260
|
+
f"[docs-audit-to-issues] codebase user view returned error for {email}: {one_line(result.stdout)}",
|
|
261
|
+
file=sys.stderr,
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
user_id = data.get("Id")
|
|
265
|
+
return str(user_id) if user_id else None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def evidence_tokens(body: str) -> list:
|
|
269
|
+
"""Extract backtick-wrapped repo-relative paths from an issue body."""
|
|
270
|
+
# Tolerate an optional `:line` / `:line-line` suffix (real reports cite
|
|
271
|
+
# `path/file.go:52`); the suffix is stripped so clustering joins on the path.
|
|
272
|
+
return re.findall(r"`([\w./-]+\.[\w]+)(?::\d+(?:-\d+)?)?`", body)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def severity_markers(finding) -> str:
|
|
276
|
+
"""Build machine-readable severity and evidence comment markers."""
|
|
277
|
+
lines = [f"<!-- severity:{finding.severity} -->"]
|
|
278
|
+
for token in evidence_tokens(finding.body):
|
|
279
|
+
lines.append(f"<!-- evidence:{token} -->")
|
|
280
|
+
return "\n".join(lines)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def build_issue_body(finding: Finding, report_path: Path) -> str:
|
|
284
|
+
body = f"""## Problem
|
|
285
|
+
|
|
286
|
+
docs-audit 发现文档与实现存在 `{finding.severity}` 级差异,需要代码/配置/实现侧跟进。
|
|
287
|
+
|
|
288
|
+
## Proposal
|
|
289
|
+
|
|
290
|
+
请根据差异点确认应补实现、补配置、调整行为,或协调文档侧重新收敛契约。
|
|
291
|
+
|
|
292
|
+
## Acceptance Criteria
|
|
293
|
+
|
|
294
|
+
- [ ] 差异点中的代码/配置/实现问题已确认并处理,或给出明确无需改代码的结论。
|
|
295
|
+
- [ ] 如行为发生变化,已同步稳定文档与必要测试。
|
|
296
|
+
- [ ] 相关 MR 关闭本 issue。
|
|
297
|
+
|
|
298
|
+
## Docs Audit Finding
|
|
299
|
+
|
|
300
|
+
{finding.body}
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
source: docs-audit ({report_path})
|
|
304
|
+
"""
|
|
305
|
+
return body.rstrip() + "\n\n" + severity_markers(finding) + "\n"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def first_evidence_token(body: str) -> str:
|
|
309
|
+
match = PATH_RE.search(body)
|
|
310
|
+
return match.group(1) if match else body[:120]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def parse_issue_ref(output: str) -> str | None:
|
|
314
|
+
created = re.search(r"created issue #(\d+)(?:\s+\(([^)]+)\))?", output)
|
|
315
|
+
if created:
|
|
316
|
+
number = created.group(1)
|
|
317
|
+
url = created.group(2) or ""
|
|
318
|
+
return f"#{number}{' ' + url if url else ''}"
|
|
319
|
+
existing = re.search(r"issue #(\d+) already exists", output)
|
|
320
|
+
if existing:
|
|
321
|
+
return f"#{existing.group(1)}(已存在)"
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def format_people(emails: set[str], assignee_ids: list[str], unresolved: list[str]) -> str:
|
|
326
|
+
if not emails:
|
|
327
|
+
return "未识别到 `@bytedance.com` 负责人,未指定 assignee"
|
|
328
|
+
names = ", ".join(f"`{email.split('@', 1)[0]}`" for email in sorted(emails))
|
|
329
|
+
suffix = []
|
|
330
|
+
if assignee_ids:
|
|
331
|
+
suffix.append(f"已指定 {len(assignee_ids)} 人")
|
|
332
|
+
if unresolved:
|
|
333
|
+
suffix.append("未解析:" + ", ".join(f"`{email}`" for email in unresolved))
|
|
334
|
+
return names + ("(" + ";".join(suffix) + ")" if suffix else "")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def one_line(text: str) -> str:
|
|
338
|
+
return re.sub(r"\s+", " ", text).strip()[:240] or "unknown error"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def log_issue_failure(
|
|
342
|
+
*,
|
|
343
|
+
label: str,
|
|
344
|
+
dedup_key: str,
|
|
345
|
+
assignees: str,
|
|
346
|
+
cmd: list[str],
|
|
347
|
+
result: subprocess.CompletedProcess[str],
|
|
348
|
+
) -> None:
|
|
349
|
+
print(f"[docs-audit-to-issues] issue create failed: {label}", file=sys.stderr)
|
|
350
|
+
print(f"[docs-audit-to-issues] returncode: {result.returncode}", file=sys.stderr)
|
|
351
|
+
print(f"[docs-audit-to-issues] dedup: {dedup_key}", file=sys.stderr)
|
|
352
|
+
print(f"[docs-audit-to-issues] assignees: {assignees}", file=sys.stderr)
|
|
353
|
+
print(f"[docs-audit-to-issues] command: {shlex.join(cmd)}", file=sys.stderr)
|
|
354
|
+
print_block("[docs-audit-to-issues] stdout", result.stdout)
|
|
355
|
+
print_block("[docs-audit-to-issues] stderr", result.stderr)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def print_block(label: str, text: str, limit: int = 4000) -> None:
|
|
359
|
+
text = text.strip()
|
|
360
|
+
if not text:
|
|
361
|
+
print(f"{label}: <empty>", file=sys.stderr)
|
|
362
|
+
return
|
|
363
|
+
if len(text) > limit:
|
|
364
|
+
text = text[:limit] + "\n...<truncated>"
|
|
365
|
+
print(f"{label}:\n{text}", file=sys.stderr)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def write_summary(path: Path, lines: list[str]) -> None:
|
|
369
|
+
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == "__main__":
|
|
373
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Non-blocking pre-commit nudge for Docs Verification Gate.
|
|
3
|
+
#
|
|
4
|
+
# If staged changes touch implementation/config/contract paths but no docs/
|
|
5
|
+
# files, remind the author to check whether documentation needs updates. This
|
|
6
|
+
# is only a nudge, not a real docs audit. OpenAPI generated docs are excluded
|
|
7
|
+
# because they move with the API generation gate.
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
staged="$(git diff --cached --name-only --diff-filter=ACMRD 2>/dev/null || true)"
|
|
16
|
+
if [ -z "$staged" ]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
has_risk=0
|
|
21
|
+
has_docs=0
|
|
22
|
+
risk_paths=()
|
|
23
|
+
|
|
24
|
+
while IFS= read -r path; do
|
|
25
|
+
[ -z "$path" ] && continue
|
|
26
|
+
case "$path" in
|
|
27
|
+
docs/openapi/*)
|
|
28
|
+
;;
|
|
29
|
+
docs/*)
|
|
30
|
+
has_docs=1
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
case "$path" in
|
|
35
|
+
api/idl/* | \
|
|
36
|
+
cmd/* | \
|
|
37
|
+
internal/* | \
|
|
38
|
+
pkg/* | \
|
|
39
|
+
configs/* | \
|
|
40
|
+
deployments/* | \
|
|
41
|
+
migration/* | \
|
|
42
|
+
migrations/* | \
|
|
43
|
+
build/docker/* | \
|
|
44
|
+
coding-otelcol/*)
|
|
45
|
+
has_risk=1
|
|
46
|
+
risk_paths+=("$path")
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
done <<<"$staged"
|
|
50
|
+
|
|
51
|
+
if [ "$has_risk" -eq 1 ] && [ "$has_docs" -eq 0 ]; then
|
|
52
|
+
printf '%s\n' \
|
|
53
|
+
"Reminder: staged changes touch implementation/config/contract paths but no docs/ files." \
|
|
54
|
+
"Run the AGENTS.md Docs Verification Gate: grep docs/ for changed identifiers," \
|
|
55
|
+
"update stable docs if behavior changed, or be ready to explain why docs do not apply." >&2
|
|
56
|
+
printf 'Changed paths that triggered this reminder:\n' >&2
|
|
57
|
+
printf ' - %s\n' "${risk_paths[@]:0:8}" >&2
|
|
58
|
+
if [ "${#risk_paths[@]}" -gt 8 ]; then
|
|
59
|
+
printf ' - ... (%d more)\n' "$((${#risk_paths[@]} - 8))" >&2
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
exit 0
|