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,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Hard guardrail: block MRs whose diff exceeds 8000 lines of human-authored
|
|
3
|
+
# change against the target branch.
|
|
4
|
+
#
|
|
5
|
+
# Rationale: large MRs are unreviewable in practice. AGENTS.md / vibe-coding.html
|
|
6
|
+
# already recommend splitting at 200 lines; this script enforces a non-negotiable
|
|
7
|
+
# ceiling at the MR layer. Generated files (lockfiles, auto-gen typings,
|
|
8
|
+
# i18n locales, vendored assets) are excluded from the count -- they routinely
|
|
9
|
+
# balloon legitimately and should not block review.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# scripts/check-mr-size.sh # diff against origin/release
|
|
13
|
+
# scripts/check-mr-size.sh --base main # diff against another base
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 under ceiling
|
|
17
|
+
# 1 over ceiling -- MR creation should be aborted
|
|
18
|
+
# 2 bad usage / missing dependency
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
cd "$(dirname "$0")/.."
|
|
22
|
+
|
|
23
|
+
# shellcheck source=scripts/lib/cli-args.sh
|
|
24
|
+
. scripts/lib/cli-args.sh
|
|
25
|
+
|
|
26
|
+
MAX_LINES=8000
|
|
27
|
+
|
|
28
|
+
BASE=""
|
|
29
|
+
while [ $# -gt 0 ]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--base) BASE=$(require_value --base "${2:-}"); shift 2 ;;
|
|
32
|
+
--base=*) BASE="${1#--base=}"; shift ;;
|
|
33
|
+
-h|--help)
|
|
34
|
+
sed -n '2,20p' "$0"; exit 0 ;;
|
|
35
|
+
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
# Default base: prefer origin/release (project convention per open-mr.sh),
|
|
40
|
+
# fall back to origin/main, then main.
|
|
41
|
+
if [ -z "$BASE" ]; then
|
|
42
|
+
for candidate in origin/release origin/main main; do
|
|
43
|
+
if git rev-parse --verify --quiet "$candidate" >/dev/null; then
|
|
44
|
+
BASE="$candidate"; break
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
fi
|
|
48
|
+
if [ -z "$BASE" ]; then
|
|
49
|
+
echo "error: could not resolve a base branch; pass --base <ref>" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Validate BASE resolves to a commit. Without this, an invalid ref (typo,
|
|
54
|
+
# unfetched remote branch) makes `git diff` fail under `< <(...)`, where
|
|
55
|
+
# `set -euo pipefail` does NOT propagate the failure -- awk's END block
|
|
56
|
+
# then emits "0 0" and the guardrail silently passes, defeating its purpose.
|
|
57
|
+
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
58
|
+
echo "error: base ref does not resolve to a commit: ${BASE}" >&2
|
|
59
|
+
echo " did you forget to 'git fetch origin'?" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Three-dot diff -- count changes introduced on this branch since it forked
|
|
64
|
+
# from BASE, NOT changes that landed on BASE in the meantime.
|
|
65
|
+
#
|
|
66
|
+
# Exclusions: lockfiles, auto-generated typings, i18n locale bundles, vendored
|
|
67
|
+
# build artifacts, generated Go / API-doc bundles, and generated deployment SQL.
|
|
68
|
+
# Keep this list narrow -- every exclusion is a hole. The Go-codegen + openapi
|
|
69
|
+
# patterns mirror the "generated code" set already used by
|
|
70
|
+
# scripts/check-comment-i18n.sh and .golangci.yml; regenerating from a source
|
|
71
|
+
# (IDL / go:generate / redocly / migration SQL) legitimately balloons these and
|
|
72
|
+
# must not count against review size.
|
|
73
|
+
EXCLUDES=(
|
|
74
|
+
':(exclude)**/pnpm-lock.yaml'
|
|
75
|
+
':(exclude)**/package-lock.json'
|
|
76
|
+
':(exclude)**/yarn.lock'
|
|
77
|
+
':(exclude)**/go.sum'
|
|
78
|
+
':(exclude)**/Cargo.lock'
|
|
79
|
+
':(exclude)web/packages/api/src/bam-auto-gen/**'
|
|
80
|
+
':(exclude)web/apps/*/locales/**'
|
|
81
|
+
':(exclude)**/*.snap'
|
|
82
|
+
':(exclude)**/testdata/**'
|
|
83
|
+
':(exclude)**/vendor/**'
|
|
84
|
+
':(exclude)**/dist/**'
|
|
85
|
+
':(exclude)**/build/**'
|
|
86
|
+
':(exclude)**/thrift_gen/**'
|
|
87
|
+
':(exclude)**/*.pb.go'
|
|
88
|
+
':(exclude)**/*_gen.go'
|
|
89
|
+
':(exclude)**/mock_*.go'
|
|
90
|
+
':(exclude)**/mocks/**'
|
|
91
|
+
':(exclude)**/zz_generated*'
|
|
92
|
+
':(exclude)docs/openapi/openapi.yaml'
|
|
93
|
+
':(exclude)docs/openapi/*.html'
|
|
94
|
+
':(exclude)deployments/coding-engine/sql/**'
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# numstat: <added>\t<deleted>\t<path>. Binary files emit '-'; treat as 0.
|
|
98
|
+
read -r ADDED DELETED < <(
|
|
99
|
+
git diff --numstat "${BASE}...HEAD" -- . "${EXCLUDES[@]}" \
|
|
100
|
+
| awk '
|
|
101
|
+
$1 == "-" { next }
|
|
102
|
+
{ a += $1; d += $2 }
|
|
103
|
+
END { printf "%d %d\n", a+0, d+0 }
|
|
104
|
+
'
|
|
105
|
+
)
|
|
106
|
+
TOTAL=$((ADDED + DELETED))
|
|
107
|
+
|
|
108
|
+
printf " base: %s\n" "$BASE"
|
|
109
|
+
printf " added: %d\n" "$ADDED"
|
|
110
|
+
printf " deleted: %d\n" "$DELETED"
|
|
111
|
+
printf " total: %d (excluding generated files)\n" "$TOTAL"
|
|
112
|
+
printf " ceiling: %d\n" "$MAX_LINES"
|
|
113
|
+
|
|
114
|
+
if [ "$TOTAL" -le "$MAX_LINES" ]; then
|
|
115
|
+
echo " [OK] under MR size ceiling"
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
cat >&2 <<EOF
|
|
120
|
+
|
|
121
|
+
[BLOCK] MR diff is ${TOTAL} lines, exceeding ceiling of ${MAX_LINES}.
|
|
122
|
+
|
|
123
|
+
Large MRs are unreviewable. Split this branch into reviewable slices:
|
|
124
|
+
- separate refactor commits from behavior changes
|
|
125
|
+
- extract pure-mechanical renames into their own MR
|
|
126
|
+
- land scaffolding before features that depend on it
|
|
127
|
+
EOF
|
|
128
|
+
exit 1
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Hard guardrail: block MRs whose title doesn't follow Conventional Commits.
|
|
3
|
+
#
|
|
4
|
+
# Same format every commit on main/release already uses. Pairs with
|
|
5
|
+
# scripts/check-mr-desc.sh; both are invoked from .gitlab-ci.yml on every MR
|
|
6
|
+
# pipeline.
|
|
7
|
+
#
|
|
8
|
+
# Title source, checked in order:
|
|
9
|
+
# 1. --title "<text>" (local)
|
|
10
|
+
# 2. $CI_MERGE_REQUEST_TITLE env (GitLab built-in; not truncated)
|
|
11
|
+
#
|
|
12
|
+
# Rules (medium-strictness):
|
|
13
|
+
# - Length: <= 72 chars (after stripping any "Draft: " / "WIP: " prefix)
|
|
14
|
+
# - No trailing '.'
|
|
15
|
+
# - Format: <type>(<scope>)?!?: <subject>
|
|
16
|
+
# - Subject: >= 5 chars
|
|
17
|
+
# - Exception: GitLab default revert titles `Revert "..."` are exempt
|
|
18
|
+
#
|
|
19
|
+
# Exit codes:
|
|
20
|
+
# 0 title is acceptable
|
|
21
|
+
# 1 title fails one or more checks -- MR should be blocked
|
|
22
|
+
# 2 bad usage / no title provided
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
cd "$(dirname "$0")/.."
|
|
26
|
+
|
|
27
|
+
# shellcheck source=scripts/lib/cli-args.sh
|
|
28
|
+
. scripts/lib/cli-args.sh
|
|
29
|
+
# shellcheck source=scripts/lib/mr-conventions.sh
|
|
30
|
+
. scripts/lib/mr-conventions.sh
|
|
31
|
+
|
|
32
|
+
TITLE_MAX_LEN=72
|
|
33
|
+
SUBJECT_MIN_LEN=5
|
|
34
|
+
SCOPE_RE='(\([a-z0-9._-]+\))?'
|
|
35
|
+
TITLE_RE="^${CC_TYPE_RE}${SCOPE_RE}!?: .{${SUBJECT_MIN_LEN},}\$"
|
|
36
|
+
|
|
37
|
+
TITLE=""
|
|
38
|
+
source_label=""
|
|
39
|
+
while [ $# -gt 0 ]; do
|
|
40
|
+
case "$1" in
|
|
41
|
+
--title)
|
|
42
|
+
TITLE=$(require_value --title "${2:-}"); shift 2 ;;
|
|
43
|
+
--title=*)
|
|
44
|
+
TITLE="${1#--title=}"; shift ;;
|
|
45
|
+
-h|--help)
|
|
46
|
+
sed -n '2,23p' "$0"; exit 0 ;;
|
|
47
|
+
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
48
|
+
esac
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
if [ -n "$TITLE" ]; then
|
|
52
|
+
source_label="arg"
|
|
53
|
+
elif [ -n "${CI_MERGE_REQUEST_TITLE:-}" ]; then
|
|
54
|
+
TITLE="$CI_MERGE_REQUEST_TITLE"
|
|
55
|
+
source_label="env:CI_MERGE_REQUEST_TITLE"
|
|
56
|
+
elif [ -n "${CI_MERGE_REQUEST_IID:-}" ]; then
|
|
57
|
+
echo "error: running on an MR pipeline but \$CI_MERGE_REQUEST_TITLE is unset." >&2
|
|
58
|
+
exit 2
|
|
59
|
+
else
|
|
60
|
+
echo "error: not running on an MR pipeline and no --title given." >&2
|
|
61
|
+
echo " see --help for local usage." >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Strip GitLab's auto-prepended draft prefixes before validation; draft status
|
|
66
|
+
# is metadata, not part of the title the author chose.
|
|
67
|
+
stripped="$TITLE"
|
|
68
|
+
stripped="${stripped#Draft: }"
|
|
69
|
+
stripped="${stripped#WIP: }"
|
|
70
|
+
# POSIX whitespace strip via parameter expansion -- no subshell, no awk fork.
|
|
71
|
+
stripped="${stripped#"${stripped%%[![:space:]]*}"}"
|
|
72
|
+
stripped="${stripped%"${stripped##*[![:space:]]}"}"
|
|
73
|
+
|
|
74
|
+
len=${#stripped}
|
|
75
|
+
|
|
76
|
+
printf " source: %s\n" "$source_label"
|
|
77
|
+
printf " title: %s\n" "$TITLE"
|
|
78
|
+
printf " length: %d (max %d)\n" "$len" "$TITLE_MAX_LEN"
|
|
79
|
+
|
|
80
|
+
# Exempt: `Revert "..."` titles created by the GitLab "Revert" button. The
|
|
81
|
+
# revert commit preserves the original conventional title in its body; the
|
|
82
|
+
# wrapping is not under the author's control.
|
|
83
|
+
if [[ "$stripped" == Revert\ \"*\" ]]; then
|
|
84
|
+
echo " [OK] revert title -- exempt from format check"
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
failures=()
|
|
89
|
+
|
|
90
|
+
if [ "$len" -gt "$TITLE_MAX_LEN" ]; then
|
|
91
|
+
failures+=("title is ${len} chars; max is ${TITLE_MAX_LEN}")
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [[ "$stripped" == *. ]]; then
|
|
95
|
+
failures+=("title ends with '.' -- drop the trailing period")
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if ! [[ "$stripped" =~ $TITLE_RE ]]; then
|
|
99
|
+
failures+=("title does not match Conventional Commits: <type>(<scope>)?!?: <subject> (subject >= ${SUBJECT_MIN_LEN} chars)")
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if [ "${#failures[@]}" -eq 0 ]; then
|
|
103
|
+
echo " [OK] MR title passes lint"
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
echo
|
|
109
|
+
echo " [BLOCK] MR title failed ${#failures[@]} check(s):"
|
|
110
|
+
for f in "${failures[@]}"; do
|
|
111
|
+
echo " - $f"
|
|
112
|
+
done
|
|
113
|
+
echo
|
|
114
|
+
echo " Allowed types: ${CC_TYPES}"
|
|
115
|
+
echo " Examples:"
|
|
116
|
+
echo " feat(ci): add MR title lint"
|
|
117
|
+
echo " fix(redislane): skip own consumer PEL to stop self-claim storm"
|
|
118
|
+
echo " refactor!: drop deprecated v1 client"
|
|
119
|
+
echo
|
|
120
|
+
echo " How to fix: edit the title in the GitLab MR UI, or"
|
|
121
|
+
echo " bytedcli codebase mr update --number <N> --title \"<new title>\""
|
|
122
|
+
} >&2
|
|
123
|
+
exit 1
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate OpenAPI docs from Coding TOP-style Thrift IDL.
|
|
3
|
+
|
|
4
|
+
The source IDL uses TOP actions, while thrift-gen-http-swagger expects Hertz
|
|
5
|
+
HTTP annotations. This script creates a temporary OpenAPI-only Thrift file with
|
|
6
|
+
synthetic `api.post` and `api.body` annotations, delegates schema/path
|
|
7
|
+
generation to thrift-gen-http-swagger, then uses Redocly to render the HTML.
|
|
8
|
+
The synthetic paths are documentation-only TOP Action examples.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import re
|
|
15
|
+
import shlex
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
METHOD_RE = re.compile(
|
|
24
|
+
r"^(?P<indent>\s*)"
|
|
25
|
+
r"(?P<response>[A-Za-z_][A-Za-z0-9_.<>]*)\s+"
|
|
26
|
+
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)"
|
|
27
|
+
r"\(1:\s*(?P<request>[A-Za-z_][A-Za-z0-9_.<>]*)\s+[A-Za-z_][A-Za-z0-9_]*\)"
|
|
28
|
+
r"\s+throws\s+\([^)]*\)\s*"
|
|
29
|
+
r"\((?P<annotations>.*)\)\s*$"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
INSTALL_HINTS = {
|
|
33
|
+
"thriftgo": "go install github.com/cloudwego/thriftgo@v0.4.5",
|
|
34
|
+
"thrift-gen-http-swagger": (
|
|
35
|
+
"go install github.com/hertz-contrib/swagger-generate/thrift-gen-http-swagger@latest"
|
|
36
|
+
),
|
|
37
|
+
"redocly": "npm install -g @redocly/cli@2.31.6",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run(cmd: list[str], cwd: Path) -> None:
|
|
42
|
+
try:
|
|
43
|
+
subprocess.run(cmd, cwd=cwd, check=True)
|
|
44
|
+
except FileNotFoundError as exc:
|
|
45
|
+
executable = Path(exc.filename or cmd[0]).name
|
|
46
|
+
print(f"missing executable: {executable}", file=sys.stderr)
|
|
47
|
+
if hint := INSTALL_HINTS.get(executable):
|
|
48
|
+
print(f"install it with: {hint}", file=sys.stderr)
|
|
49
|
+
raise SystemExit(1) from exc
|
|
50
|
+
except subprocess.CalledProcessError as exc:
|
|
51
|
+
print(f"command failed ({exc.returncode}): {' '.join(cmd)}", file=sys.stderr)
|
|
52
|
+
raise SystemExit(exc.returncode) from exc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def ensure_executable(command: str, tool_name: str) -> None:
|
|
56
|
+
if shutil.which(command) is not None:
|
|
57
|
+
return
|
|
58
|
+
print(f"missing executable for {tool_name}: {command}", file=sys.stderr)
|
|
59
|
+
if hint := INSTALL_HINTS.get(tool_name):
|
|
60
|
+
print(f"install it with: {hint}", file=sys.stderr)
|
|
61
|
+
raise SystemExit(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_command(command: str, tool_name: str) -> list[str]:
|
|
65
|
+
parts = shlex.split(command)
|
|
66
|
+
if not parts:
|
|
67
|
+
print(f"missing executable for {tool_name}: {command}", file=sys.stderr)
|
|
68
|
+
if hint := INSTALL_HINTS.get(tool_name):
|
|
69
|
+
print(f"install it with: {hint}", file=sys.stderr)
|
|
70
|
+
raise SystemExit(1)
|
|
71
|
+
ensure_executable(parts[0], tool_name)
|
|
72
|
+
return parts
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def rewrite_server_idl(source: Path, destination: Path, top_version: str) -> None:
|
|
76
|
+
wrappers: list[str] = []
|
|
77
|
+
rewritten: list[str] = []
|
|
78
|
+
for line in source.read_text(encoding="utf-8").splitlines():
|
|
79
|
+
match = METHOD_RE.match(line)
|
|
80
|
+
if match is None:
|
|
81
|
+
rewritten.append(line)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
name = match.group("name")
|
|
85
|
+
request_type = match.group("request")
|
|
86
|
+
response_type = match.group("response")
|
|
87
|
+
request_wrapper = f"{name}OpenAPIRequest"
|
|
88
|
+
response_wrapper = f"{name}OpenAPIResponse"
|
|
89
|
+
wrappers.extend(
|
|
90
|
+
[
|
|
91
|
+
f"struct {request_wrapper} {{",
|
|
92
|
+
f' 1: {request_type} Body (api.body = "body")',
|
|
93
|
+
"}",
|
|
94
|
+
"",
|
|
95
|
+
f"struct {response_wrapper} {{",
|
|
96
|
+
f' 1: {response_type} Body (api.body = "body")',
|
|
97
|
+
"}",
|
|
98
|
+
"",
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
rewritten.append(
|
|
102
|
+
f"{match.group('indent')}{response_wrapper} {name}"
|
|
103
|
+
f"(1: {request_wrapper} req) throws (1: base.Error err) "
|
|
104
|
+
f"({match.group('annotations')}, api.post=\"/?Action={name}&Version={top_version}\")"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
text = "\n".join(rewritten) + "\n"
|
|
108
|
+
insert_at = text.find("// Agent 管理接口")
|
|
109
|
+
if insert_at < 0:
|
|
110
|
+
print("cannot find insertion point in server thrift", file=sys.stderr)
|
|
111
|
+
raise SystemExit(1)
|
|
112
|
+
destination.write_text(text[:insert_at] + "\n".join(wrappers) + text[insert_at:], encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def prepare_workdir(idl: Path, workdir: Path, top_version: str) -> Path:
|
|
116
|
+
idl_dir = idl.parent
|
|
117
|
+
for thrift_file in idl_dir.glob("*.thrift"):
|
|
118
|
+
shutil.copy2(thrift_file, workdir / thrift_file.name)
|
|
119
|
+
|
|
120
|
+
rewritten = workdir / idl.name
|
|
121
|
+
rewrite_server_idl(idl, rewritten, top_version)
|
|
122
|
+
return rewritten
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def customize_openapi_info(path: Path) -> None:
|
|
126
|
+
text = path.read_text(encoding="utf-8")
|
|
127
|
+
text = text.replace(
|
|
128
|
+
" title: API generated by thrift-gen-http-swagger\n"
|
|
129
|
+
" description: API description\n",
|
|
130
|
+
" title: Coding API 文档\n"
|
|
131
|
+
" description: Coding Engine TOP API reference generated from Thrift IDL. "
|
|
132
|
+
"Paths are illustrative TOP Action examples.\n",
|
|
133
|
+
1,
|
|
134
|
+
)
|
|
135
|
+
path.write_text(text, encoding="utf-8")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def generate_openapi(
|
|
139
|
+
idl: Path,
|
|
140
|
+
yaml_path: Path,
|
|
141
|
+
html_path: Path,
|
|
142
|
+
redocly_config: Path,
|
|
143
|
+
top_version: str,
|
|
144
|
+
thriftgo: str,
|
|
145
|
+
swagger_plugin: str,
|
|
146
|
+
redocly: str,
|
|
147
|
+
) -> None:
|
|
148
|
+
ensure_executable(thriftgo, "thriftgo")
|
|
149
|
+
ensure_executable(swagger_plugin, "thrift-gen-http-swagger")
|
|
150
|
+
redocly_cmd = parse_command(redocly, "redocly")
|
|
151
|
+
|
|
152
|
+
with tempfile.TemporaryDirectory(prefix="coding-openapi-") as tmp:
|
|
153
|
+
workdir = Path(tmp)
|
|
154
|
+
rewritten = prepare_workdir(idl, workdir, top_version)
|
|
155
|
+
generated_dir = workdir / "swagger"
|
|
156
|
+
generated_dir.mkdir()
|
|
157
|
+
|
|
158
|
+
run(
|
|
159
|
+
[
|
|
160
|
+
thriftgo,
|
|
161
|
+
"-i",
|
|
162
|
+
".",
|
|
163
|
+
"-o",
|
|
164
|
+
"gen",
|
|
165
|
+
"-g",
|
|
166
|
+
"go:skip_go_gen",
|
|
167
|
+
"-p",
|
|
168
|
+
f"http-swagger={swagger_plugin}:OutputDir={generated_dir}",
|
|
169
|
+
rewritten.name,
|
|
170
|
+
],
|
|
171
|
+
cwd=workdir,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
generated_yaml = generated_dir / "openapi.yaml"
|
|
175
|
+
if not generated_yaml.exists():
|
|
176
|
+
print("thrift-gen-http-swagger did not produce openapi.yaml", file=sys.stderr)
|
|
177
|
+
raise SystemExit(1)
|
|
178
|
+
|
|
179
|
+
customize_openapi_info(generated_yaml)
|
|
180
|
+
yaml_path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
html_path.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
shutil.copy2(generated_yaml, yaml_path)
|
|
183
|
+
run(redocly_cmd + ["lint", str(yaml_path), "--config", str(redocly_config)], cwd=Path.cwd())
|
|
184
|
+
run(
|
|
185
|
+
redocly_cmd + ["build-docs", str(yaml_path), "-o", str(html_path), "--config", str(redocly_config)],
|
|
186
|
+
cwd=Path.cwd(),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main() -> None:
|
|
191
|
+
parser = argparse.ArgumentParser(description="Generate OpenAPI docs from Thrift IDL.")
|
|
192
|
+
parser.add_argument("--idl", default="api/idl/server.thrift", help="entry thrift file")
|
|
193
|
+
parser.add_argument("--yaml", default="docs/openapi/openapi.yaml", help="OpenAPI YAML output")
|
|
194
|
+
parser.add_argument("--html", default="docs/openapi/coding-engine-api.html", help="Redocly HTML output")
|
|
195
|
+
parser.add_argument("--redocly-config", default="redocly.yaml")
|
|
196
|
+
parser.add_argument("--top-version", default="2026-04-23", help="TOP API Version query value")
|
|
197
|
+
parser.add_argument("--thriftgo", default="thriftgo")
|
|
198
|
+
parser.add_argument("--swagger-plugin", default="thrift-gen-http-swagger")
|
|
199
|
+
parser.add_argument("--redocly", default="redocly")
|
|
200
|
+
args = parser.parse_args()
|
|
201
|
+
|
|
202
|
+
idl = Path(args.idl)
|
|
203
|
+
if not idl.exists():
|
|
204
|
+
print(f"{idl} not found", file=sys.stderr)
|
|
205
|
+
raise SystemExit(1)
|
|
206
|
+
|
|
207
|
+
generate_openapi(
|
|
208
|
+
idl=idl,
|
|
209
|
+
yaml_path=Path(args.yaml),
|
|
210
|
+
html_path=Path(args.html),
|
|
211
|
+
redocly_config=Path(args.redocly_config),
|
|
212
|
+
top_version=args.top_version,
|
|
213
|
+
thriftgo=args.thriftgo,
|
|
214
|
+
swagger_plugin=args.swagger_plugin,
|
|
215
|
+
redocly=args.redocly,
|
|
216
|
+
)
|
|
217
|
+
print(f"updated {args.yaml} and {args.html}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
root="${1:-.}"
|
|
5
|
+
paths=(
|
|
6
|
+
"$root/internal/adapters/outbound/persistence/rdb/repository"
|
|
7
|
+
"$root/mem-svc/internal/adapters/outbound/persistence/rdb/repository"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
existing=()
|
|
11
|
+
for path in "${paths[@]}"; do
|
|
12
|
+
if [[ -d "$path" ]]; then
|
|
13
|
+
existing+=("$path")
|
|
14
|
+
fi
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
if [[ ${#existing[@]} -eq 0 ]]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
python3 - "$root" "${existing[@]}" <<'PY'
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
_, root, *dirs = sys.argv
|
|
27
|
+
root_path = Path(root).resolve()
|
|
28
|
+
violations = []
|
|
29
|
+
|
|
30
|
+
db_branch_re = re.compile(
|
|
31
|
+
r'\b(?:Dialector\.Name|driver|dbType|databaseType)\b.*(?:==|!=|switch|case)'
|
|
32
|
+
r'|\b(?:if|switch|case)\b.*\b(?:Dialector\.Name|driver|dbType|databaseType)\b'
|
|
33
|
+
)
|
|
34
|
+
raw_call_re = re.compile(r'\.(?:Raw|Exec)\s*\(')
|
|
35
|
+
direct_join_re = re.compile(r'\.Joins\s*\(\s*(?:"|`)JOIN\s+(?!\?)', re.IGNORECASE)
|
|
36
|
+
select_literal_re = re.compile(r'\.Select\s*\(\s*(?P<quote>"|`)(?P<body>.*)', re.IGNORECASE)
|
|
37
|
+
where_literal_re = re.compile(r'\.Where\s*\(\s*(?P<quote>"|`)(?P<body>.*)', re.IGNORECASE)
|
|
38
|
+
def allowed(lines, index):
|
|
39
|
+
current = lines[index]
|
|
40
|
+
previous = lines[index - 1] if index > 0 else ""
|
|
41
|
+
return "coding:allow-raw-sql" in current or "coding:allow-raw-sql" in previous
|
|
42
|
+
|
|
43
|
+
def literal_body(lines, start, quote):
|
|
44
|
+
text = lines[start].split(quote, 1)[1]
|
|
45
|
+
if quote == '"' and quote in text:
|
|
46
|
+
return text.split(quote, 1)[0]
|
|
47
|
+
if quote == '`' and '`' in text:
|
|
48
|
+
return text.split('`', 1)[0]
|
|
49
|
+
for line in lines[start + 1:]:
|
|
50
|
+
text += "\n" + line
|
|
51
|
+
if quote in line:
|
|
52
|
+
return text.split(quote, 1)[0]
|
|
53
|
+
return text
|
|
54
|
+
|
|
55
|
+
for directory in dirs:
|
|
56
|
+
for path in Path(directory).rglob("*.go"):
|
|
57
|
+
if path.name.endswith("_test.go"):
|
|
58
|
+
continue
|
|
59
|
+
lines = path.read_text().splitlines()
|
|
60
|
+
rel = path.resolve().relative_to(root_path)
|
|
61
|
+
for i, line in enumerate(lines):
|
|
62
|
+
if db_branch_re.search(line):
|
|
63
|
+
violations.append(f"{rel}:{i + 1}: repository code must not branch on database driver/type")
|
|
64
|
+
if allowed(lines, i):
|
|
65
|
+
continue
|
|
66
|
+
if raw_call_re.search(line):
|
|
67
|
+
violations.append(f"{rel}:{i + 1}: repository code must not use Raw/Exec; use GORM clauses or explicit migrations")
|
|
68
|
+
if direct_join_re.search(line):
|
|
69
|
+
violations.append(f"{rel}:{i + 1}: Join table names must use JOIN ? with clause.Table and clause.Column")
|
|
70
|
+
|
|
71
|
+
select_match = select_literal_re.search(line)
|
|
72
|
+
if select_match:
|
|
73
|
+
quote = select_match.group("quote")
|
|
74
|
+
body = literal_body(lines, i, quote).strip()
|
|
75
|
+
if not body.startswith("?"):
|
|
76
|
+
violations.append(f"{rel}:{i + 1}: Select field lists must use clause.Column placeholders, not raw SQL strings")
|
|
77
|
+
|
|
78
|
+
match = where_literal_re.search(line)
|
|
79
|
+
if not match:
|
|
80
|
+
continue
|
|
81
|
+
quote = match.group("quote")
|
|
82
|
+
body = literal_body(lines, i, quote).strip()
|
|
83
|
+
upper = " ".join(body.upper().split())
|
|
84
|
+
if upper in {"EXISTS (?)", "NOT EXISTS (?)"}:
|
|
85
|
+
continue
|
|
86
|
+
violations.append(f"{rel}:{i + 1}: Where predicates must use clause.Expr/clause.Eq/clause.And helpers, not raw SQL strings")
|
|
87
|
+
|
|
88
|
+
if violations:
|
|
89
|
+
print("\n".join(violations), file=sys.stderr)
|
|
90
|
+
print(
|
|
91
|
+
"\nRDB repositories must use structured GORM queries so every dialect quotes identifiers consistently.\n"
|
|
92
|
+
"Use clause.Table/clause.Column for joins and clause.Eq/IN/And/Or or repository helpers for predicates.\n"
|
|
93
|
+
"Do not branch on database type in repository code. If a rare raw SQL escape is unavoidable, add\n"
|
|
94
|
+
"'// coding:allow-raw-sql <reason>' on the previous line and explain why structured clauses cannot model it.",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
PY
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
root="${1:-.}"
|
|
5
|
+
patterns='\.(Transaction|Begin|Commit|Rollback|BeginTx|CommitTx|RollbackTx)\('
|
|
6
|
+
legacy_tx_probe='rdb\.DB\([^)]*,[[:space:]]*nil\)'
|
|
7
|
+
paths=(
|
|
8
|
+
"$root/internal/adapters/outbound/persistence/rdb/repository"
|
|
9
|
+
"$root/mem-svc/internal/adapters/outbound/persistence/rdb/repository"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
existing=()
|
|
13
|
+
for path in "${paths[@]}"; do
|
|
14
|
+
if [[ -d "$path" ]]; then
|
|
15
|
+
existing+=("$path")
|
|
16
|
+
fi
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
if [[ ${#existing[@]} -eq 0 ]]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if matches="$(grep -rn --include='*.go' -E "$patterns" "${existing[@]}" 2>&1)"; then
|
|
24
|
+
status=0
|
|
25
|
+
else
|
|
26
|
+
status=$?
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ $status -eq 0 ]]; then
|
|
30
|
+
printf '%s\n' "$matches"
|
|
31
|
+
printf '\nRepository layer must not open or control transactions. Use port.TransactionManager from application services.\n' >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ $status -gt 1 ]]; then
|
|
36
|
+
printf '%s\n' "$matches" >&2
|
|
37
|
+
exit "$status"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if legacy_matches="$(grep -rn --include='*.go' -E "$legacy_tx_probe" "${existing[@]}" 2>&1)"; then
|
|
41
|
+
status=0
|
|
42
|
+
else
|
|
43
|
+
status=$?
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [[ $status -eq 0 ]]; then
|
|
47
|
+
printf '%s\n' "$legacy_matches"
|
|
48
|
+
printf '\nRepository transaction-required checks must use rdb.RequireTransaction(ctx), not rdb.DB(ctx, nil).\n' >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if [[ $status -gt 1 ]]; then
|
|
53
|
+
printf '%s\n' "$legacy_matches" >&2
|
|
54
|
+
exit "$status"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
python3 - "$root" "${existing[@]}" <<'PY'
|
|
58
|
+
import re
|
|
59
|
+
import sys
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
_, root, *dirs = sys.argv
|
|
63
|
+
violations = []
|
|
64
|
+
func_re = re.compile(r'^func\s+(?:\([^)]*\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\(')
|
|
65
|
+
|
|
66
|
+
for directory in dirs:
|
|
67
|
+
for path in Path(directory).rglob("*.go"):
|
|
68
|
+
if path.name.endswith("_test.go"):
|
|
69
|
+
continue
|
|
70
|
+
lines = path.read_text().splitlines()
|
|
71
|
+
i = 0
|
|
72
|
+
while i < len(lines):
|
|
73
|
+
match = func_re.match(lines[i])
|
|
74
|
+
if not match:
|
|
75
|
+
i += 1
|
|
76
|
+
continue
|
|
77
|
+
start = i
|
|
78
|
+
depth = 0
|
|
79
|
+
seen_open = False
|
|
80
|
+
body = []
|
|
81
|
+
while i < len(lines):
|
|
82
|
+
line = lines[i]
|
|
83
|
+
body.append(line)
|
|
84
|
+
depth += line.count("{")
|
|
85
|
+
if "{" in line:
|
|
86
|
+
seen_open = True
|
|
87
|
+
depth -= line.count("}")
|
|
88
|
+
i += 1
|
|
89
|
+
if seen_open and depth == 0:
|
|
90
|
+
break
|
|
91
|
+
text = "\n".join(body)
|
|
92
|
+
if "clause.Locking" in text and "rdb.RequireTransaction(" not in text:
|
|
93
|
+
rel = path.relative_to(root)
|
|
94
|
+
violations.append(f"{rel}:{start + 1}: {match.group(1)} uses clause.Locking without rdb.RequireTransaction")
|
|
95
|
+
|
|
96
|
+
if violations:
|
|
97
|
+
print("\n".join(violations), file=sys.stderr)
|
|
98
|
+
print("\nRepository methods that issue row locks must require an application-layer transaction.", file=sys.stderr)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
PY
|