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,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