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.
Files changed (57) hide show
  1. package/README.md +91 -0
  2. package/bin/coding-engine.js +16 -0
  3. package/lib/cli.js +86 -0
  4. package/lib/index.js +6 -0
  5. package/lib/setup.js +290 -0
  6. package/package.json +32 -0
  7. package/templates/.agents/skills/coding-architecture/SKILL.md +347 -0
  8. package/templates/.agents/skills/coding-code-review/SKILL.md +95 -0
  9. package/templates/.agents/skills/coding-cve-remediation/SKILL.md +163 -0
  10. package/templates/.agents/skills/coding-db-schema/SKILL.md +273 -0
  11. package/templates/.agents/skills/coding-deploy-config/SKILL.md +129 -0
  12. package/templates/.agents/skills/coding-docs-audit/SKILL.md +285 -0
  13. package/templates/.agents/skills/coding-docs-audit-autofix/SKILL.md +33 -0
  14. package/templates/.agents/skills/coding-http-api/SKILL.md +269 -0
  15. package/templates/.agents/skills/coding-issue-autodev/SKILL.md +172 -0
  16. package/templates/.agents/skills/coding-merge-request/SKILL.md +199 -0
  17. package/templates/.agents/skills/coding-observability/SKILL.md +193 -0
  18. package/templates/.agents/skills/coding-refactor-insight/SKILL.md +89 -0
  19. package/templates/.agents/skills/coding-release-merge/SKILL.md +224 -0
  20. package/templates/.agents/skills/coding-runtime-adapter/SKILL.md +115 -0
  21. package/templates/.agents/skills/coding-testing/SKILL.md +169 -0
  22. package/templates/AGENTS.md +179 -0
  23. package/templates/MR-Template-Default.md +33 -0
  24. package/templates/Makefile +370 -0
  25. package/templates/REGISTRY.md +53 -0
  26. package/templates/scripts/check-adr.sh +283 -0
  27. package/templates/scripts/check-comment-i18n.py +209 -0
  28. package/templates/scripts/check-comment-i18n.sh +38 -0
  29. package/templates/scripts/check-doc-refs.sh +40 -0
  30. package/templates/scripts/check-docs.sh +103 -0
  31. package/templates/scripts/check-go-names.sh +59 -0
  32. package/templates/scripts/check-iam-actions.py +126 -0
  33. package/templates/scripts/check-migration-sql-immutability.sh +232 -0
  34. package/templates/scripts/check-mr-desc.sh +165 -0
  35. package/templates/scripts/check-mr-size.sh +128 -0
  36. package/templates/scripts/check-mr-title.sh +123 -0
  37. package/templates/scripts/check-openapi.py +221 -0
  38. package/templates/scripts/check-rdb-structured-queries.sh +98 -0
  39. package/templates/scripts/check-repository-transactions.sh +100 -0
  40. package/templates/scripts/check-runbooks.sh +229 -0
  41. package/templates/scripts/check-skill-router-sync.py +331 -0
  42. package/templates/scripts/check-skills.sh +243 -0
  43. package/templates/scripts/docs-audit-to-issues.py +373 -0
  44. package/templates/scripts/docs-verification-reminder.sh +63 -0
  45. package/templates/scripts/find-ci-failures.sh +133 -0
  46. package/templates/scripts/find-stale-mrs.sh +72 -0
  47. package/templates/scripts/gen-adr-index.sh +122 -0
  48. package/templates/scripts/open-mr.sh +536 -0
  49. package/templates/scripts/regen-agent-md.sh +149 -0
  50. package/templates/scripts/run-mr-hygiene.sh +140 -0
  51. package/templates/scripts/runbook.sh +91 -0
  52. package/templates/scripts/self-maintenance-digest.py +125 -0
  53. package/templates/scripts/send-self-maintenance-lark-card.py +116 -0
  54. package/templates/scripts/skill-graph.sh +114 -0
  55. package/templates/scripts/skill-impact.sh +134 -0
  56. package/templates/scripts/skill-propose.sh +302 -0
  57. 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