@antprofuse/saddle-skill 0.1.3 → 0.2.0-rc.20

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 (41) hide show
  1. package/SKILL.md +66 -23
  2. package/agents/openai.yaml +3 -3
  3. package/assets/{v1-business → registry-consumer}/Cargo.lock +117 -235
  4. package/assets/registry-consumer/Cargo.toml +20 -0
  5. package/assets/registry-consumer/src/generated_artifact.rs +858 -0
  6. package/assets/registry-consumer/src/lib.rs +12 -0
  7. package/assets/registry-consumer/tests/provider_consumer.rs +20 -0
  8. package/package-files.txt +25 -0
  9. package/package-manifest.json +112 -0
  10. package/package.json +6 -6
  11. package/references/api-boundary.md +71 -0
  12. package/references/declaration-schema.md +115 -0
  13. package/references/identity-closure.json +49 -0
  14. package/references/machine-schema.md +47 -0
  15. package/references/migration.md +24 -0
  16. package/references/public-api.snapshot +16 -0
  17. package/scripts/parameterized-capacity-facts.py +254 -0
  18. package/scripts/sync-identity-closure.py +124 -0
  19. package/scripts/validate-contract.sh +62 -0
  20. package/scripts/validate-generated-preflight.sh +120 -0
  21. package/scripts/validate-generated-project.sh +123 -0
  22. package/scripts/validate-identity-closure.py +138 -0
  23. package/scripts/validate-machine-input.sh +48 -0
  24. package/scripts/validate-managed-objects-peak.py +147 -0
  25. package/scripts/validation-cache-key.py +88 -0
  26. package/LICENSE-APACHE +0 -203
  27. package/LICENSE-MIT +0 -22
  28. package/assets/v1-business/Cargo.toml +0 -12
  29. package/assets/v1-business/schema.sql +0 -18
  30. package/assets/v1-business/src/main.rs +0 -42
  31. package/assets/v1-business/src/order.rs +0 -118
  32. package/assets/v1-business/src/user.rs +0 -54
  33. package/references/db-0.1.1.md +0 -96
  34. package/references/gates-0.1.1.md +0 -77
  35. package/references/observability-0.1.1.md +0 -69
  36. package/references/overview-0.1.1.md +0 -73
  37. package/references/runtime-0.1.1.md +0 -54
  38. package/references/service-0.1.1.md +0 -124
  39. package/references/validation-0.1.1.md +0 -50
  40. package/scripts/check-business-boundaries.sh +0 -5
  41. package/scripts/check_business_boundaries.py +0 -356
@@ -1,124 +0,0 @@
1
- # Service 契约
2
-
3
- ## 目录
4
-
5
- - [定义和实现](#定义和实现)
6
- - [描述和注册](#描述和注册)
7
- - [内部 Service 调用](#内部-service-调用)
8
- - [稳定错误](#稳定错误)
9
- - [错误模式](#错误模式)
10
-
11
- ## 定义和实现
12
-
13
- 为每个业务操作定义零大小契约类型、请求、响应和独立 Handler:
14
-
15
- ```rust
16
- use saddle::{
17
- CallContext,
18
- service::{Service, ServiceFuture, ServiceHandler},
19
- };
20
-
21
- struct GetUser;
22
-
23
- impl Service for GetUser {
24
- type Request = GetUserRequest;
25
- type Response = GetUserResponse;
26
- }
27
-
28
- impl ServiceHandler<GetUser> for GetUserHandler {
29
- fn call<'a>(
30
- &'a self,
31
- context: &'a CallContext,
32
- request: GetUserRequest,
33
- ) -> ServiceFuture<'a, GetUserResponse> {
34
- Box::pin(async move {
35
- // 只表达业务规则,并通过注入的 Saddle 能力执行调用和 I/O。
36
- Ok(GetUserResponse { exists: true })
37
- })
38
- }
39
- }
40
- ```
41
-
42
- 请求和响应必须是 `Send + 'static`。通过 JSON 暴露时,请求还必须实现
43
- `DeserializeOwned`,响应必须实现 `Serialize`。
44
-
45
- ## 描述和注册
46
-
47
- `ServiceDescriptor::new(module, service, operation)` 的三个标识必须稳定、低基数:
48
-
49
- ```rust
50
- builder.register::<GetUser, _>(
51
- ServiceDescriptor::new("user", "user", "get"),
52
- GetUserHandler::new(database.clone()),
53
- )?;
54
- builder.expose_json::<GetUser>("/users/get");
55
- ```
56
-
57
- - `module` 表示业务模块。
58
- - `service` 表示稳定的业务服务。
59
- - `operation` 表示具体操作。
60
- - 只对确实需要外部访问的 Service 调用 `expose_json`。
61
- - 由 Saddle 检查重复注册、未注册入口和依赖闭合,不要自建注册表。
62
-
63
- ## 内部 Service 调用
64
-
65
- 调用方依赖 `ServiceClient<目标契约>`,不得依赖目标 Handler:
66
-
67
- ```rust
68
- struct CreateOrderHandler {
69
- users: ServiceClient<GetUser>,
70
- }
71
-
72
- let user = self
73
- .users
74
- .call(context, GetUserRequest { user_id })
75
- .await?;
76
- ```
77
-
78
- 在 `register_with` 工厂中解析依赖,使缺失依赖在装配阶段失败:
79
-
80
- ```rust
81
- builder.register_with::<CreateOrder, CreateOrderHandler, _>(
82
- ServiceDescriptor::new("order", "order", "create"),
83
- move |services| {
84
- Ok(CreateOrderHandler::new(
85
- database,
86
- services.client::<GetUser>()?,
87
- ))
88
- },
89
- )?;
90
- ```
91
-
92
- 不要直接构造 `ServiceClient`,也不要直接调用另一个模块的 Handler。
93
-
94
- ## 稳定错误
95
-
96
- 业务预期失败使用 `SaddleError`,错误码使用稳定的大写标识:
97
-
98
- ```rust
99
- return Err(SaddleError::new(
100
- ErrorKind::InvalidArgument,
101
- "ORDER_AMOUNT_INVALID",
102
- "order amount must be greater than zero",
103
- ));
104
- ```
105
-
106
- 可用 `ErrorKind`:`InvalidArgument`、`NotFound`、`Conflict`、`Business`、
107
- `Unavailable`、`Infrastructure`、`Internal`。该枚举是非穷尽的;业务匹配时保留
108
- 通配分支。错误消息不得包含凭据、SQL 参数、完整 payload 或内部敏感细节。
109
-
110
- ## 错误模式
111
-
112
- ```rust
113
- // 错误:绕过 Saddle 的内部调用和上下文传播。
114
- let response = GetUserHandler::new(database).call(context, request).await?;
115
-
116
- // 错误:0.1.1 没有这些宏。
117
- #[saddle::service]
118
- async fn create_order(...) { ... }
119
-
120
- // 错误:Service trait 位于 saddle::service。
121
- use saddle::{Service, ServiceHandler};
122
- ```
123
-
124
- 完整组合方式见 `assets/v1-business/src/user.rs`、`order.rs` 和 `main.rs`。
@@ -1,50 +0,0 @@
1
- # 验证
2
-
3
- ## 编译
4
-
5
- 在 Saddle 仓库执行:
6
-
7
- ```bash
8
- cargo +1.85.0 fmt --manifest-path examples/v1-business/Cargo.toml -- --check
9
- cargo +1.85.0 check --locked --manifest-path examples/v1-business/Cargo.toml
10
- cargo check --locked --manifest-path examples/v1-business/Cargo.toml
11
- cargo tree --locked --manifest-path examples/v1-business/Cargo.toml \
12
- -p saddle-framework@0.1.1
13
- gates/check-business-boundaries.sh examples/v1-business/Cargo.toml
14
- ```
15
-
16
- 对安装后的 Skill,将路径替换为 `assets/v1-business/Cargo.toml`。
17
-
18
- ## 依赖边界
19
-
20
- 确认业务清单只直接声明精确版本 Saddle 门面和批准的序列化/纯计算依赖。依赖树中
21
- 出现 Tokio、Axum 或 sqlx 作为 Saddle 的传递依赖是正常的;业务 `Cargo.toml`
22
- 直接声明或业务源码直接使用它们才是旁路。
23
-
24
- ## 当前事实
25
-
26
- - Rust 1.85 和当前工具链可编译 `assets/v1-business`。
27
- - 所有 Saddle 组件解析为 0.1.1。
28
- - 示例覆盖 Service 注册与 HTTP/JSON 暴露、内部 `ServiceClient`、托管 DB、
29
- 单层事务、稳定错误和领域事件。
30
- - 示例已在 MariaDB 10.11 上完成真实 HTTP 验证:正常订单同时提交 `orders` 和
31
- `order_audit`;让第二条写入失败后返回 `db.query_failed`,第一条写入被回滚。
32
- - 示例实测 `InvalidArgument` 映射为 HTTP 400、`Business` 映射为 HTTP 422、
33
- DB 基础设施错误映射为 HTTP 500;响应头和错误体携带同一个 trace ID。
34
- - 32 位十六进制 `x-saddle-trace-id` 被继承;无效值由框架替换。日志中的外部入口、
35
- 内部 Service、DB、事务和领域事件共享 trace,并记录 commit 或 rollback。
36
-
37
- 这些 HTTP 和日志结论是 `saddle-framework 0.1.1` 配合本示例的验证基线。升级制品后
38
- 必须重新运行,不能把示例观测外推为未发布版本的承诺。
39
-
40
- ## MariaDB 部署前提
41
-
42
- 0.1.1 会校验数据库入站包上限。MariaDB 的 `max_allowed_packet` 大于框架允许值时,
43
- 示例启动返回 `db.invalid_config`;本次以 8 MiB 配置验证通过。部署前显式确认:
44
-
45
- ```sql
46
- SELECT @@global.max_allowed_packet;
47
- ```
48
-
49
- 不要为了通过启动检查提高该值;生产配置应由部署环境按 0.1.1 的 8 MiB 结果边界
50
- 统一设置。
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
- exec python3 "$script_dir/check_business_boundaries.py" "$@"
@@ -1,356 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Conservative, closed-policy gate for Saddle V1 business crates."""
3
-
4
- from __future__ import annotations
5
-
6
- import re
7
- import os
8
- import subprocess
9
- import sys
10
- import tempfile
11
- import tomllib
12
- from pathlib import Path
13
-
14
-
15
- ALLOWED_DEPENDENCIES = {"saddle-framework", "serde"}
16
- ALLOWED_TOP_LEVEL = {"package", "dependencies", "workspace"}
17
- ALLOWED_PACKAGE_KEYS = {"name", "version", "edition", "rust-version", "publish"}
18
- FORBIDDEN_MANIFEST_KEYS = {
19
- "lib", "bin", "example", "test", "bench", "target",
20
- "build-dependencies", "dev-dependencies",
21
- }
22
- SOURCE_RULES = {
23
- "THREAD": [r"\bstd\s*::\s*thread\b"],
24
- "RUNTIME": [
25
- r"\btokio\s*::\s*runtime\b", r"\basync_std\s*::\s*task\s*::\s*block_on\b",
26
- r"\bsmol\s*::\s*block_on\b",
27
- ],
28
- "TASK": [
29
- r"\btokio\s*::\s*(?:task\s*::\s*)?spawn\b", r"\bspawn_blocking\b",
30
- r"\basync_std\s*::\s*task\s*::\s*spawn\b", r"\bsmol\s*::\s*spawn\b",
31
- ],
32
- "WEB": [
33
- r"\baxum\s*::", r"\bhyper\s*::", r"\bactix_web\s*::", r"\bwarp\s*::",
34
- r"\bpoem\s*::", r"\brocket\s*::",
35
- ],
36
- "DB": [
37
- r"\bsqlx\s*::", r"\bdiesel\s*::", r"\bsea_orm\s*::",
38
- r"\b(?:MySql|Pg|Sqlite)Pool\b",
39
- ],
40
- "OBSERVABILITY": [r"\btracing_subscriber\b", r"\blog4rs\b", r"\benv_logger\b"],
41
- "IO": [
42
- r"\bstd\s*::\s*fs\b", r"\bstd\s*::\s*process\b",
43
- r"\bstd\s*::\s*io\s*::\s*(?:stdin|stdout|stderr|Read|Write|BufRead)\b",
44
- r"\bstd\s*::\s*net\s*::\s*(?:TcpStream|TcpListener|UdpSocket)\b",
45
- r"\b(?:reqwest|ureq|surf|isahc|curl|tonic|tarpc|lapin|rdkafka|nats|redis)\s*::",
46
- ],
47
- }
48
-
49
-
50
- class Gate:
51
- def __init__(self, manifest: Path) -> None:
52
- self.manifest = manifest.resolve()
53
- self.root = self.manifest.parent
54
- self.failures = 0
55
-
56
- def report(self, rule: str, detail: str) -> None:
57
- print(f"SADDLE_GATE {rule} {detail}", file=sys.stderr)
58
- self.failures += 1
59
-
60
- def check_manifest(self, data: dict) -> None:
61
- for key in data.keys() - ALLOWED_TOP_LEVEL:
62
- rule = "DEPENDENCY" if key in {"patch", "replace"} else "MANIFEST"
63
- self.report(rule, f"{self.manifest}: 不允许 Cargo 顶层配置 [{key}]")
64
- for key in FORBIDDEN_MANIFEST_KEYS & data.keys():
65
- self.report("TARGET", f"{self.manifest}: 不允许 [{key}] 或自定义/附加 Cargo target")
66
-
67
- package = data.get("package")
68
- if not isinstance(package, dict):
69
- self.report("MANIFEST", f"{self.manifest}: 缺少 [package]")
70
- else:
71
- for key in package.keys() - ALLOWED_PACKAGE_KEYS:
72
- rule = "BUILD" if key in {"build", "links"} else "TARGET"
73
- self.report(rule, f"{self.manifest}: 不允许 package.{key}")
74
-
75
- workspace = data.get("workspace")
76
- if workspace not in ({}, None):
77
- self.report("MANIFEST", f"{self.manifest}: [workspace] 不得包含配置")
78
-
79
- dependencies = data.get("dependencies")
80
- if not isinstance(dependencies, dict):
81
- self.report("DEPENDENCY", f"{self.manifest}: 缺少 [dependencies]")
82
- return
83
-
84
- for name, spec in dependencies.items():
85
- if name not in ALLOWED_DEPENDENCIES:
86
- self.report("DEPENDENCY", f"{self.manifest}: 未批准的直接依赖 {name}")
87
- continue
88
- if name == "saddle-framework":
89
- if isinstance(spec, dict) and spec.get("version") == "=0.1.1":
90
- self.report("DEPENDENCY", f"{self.manifest}: saddle-framework 禁止 path/git/registry/package 等依赖来源")
91
- elif spec != "=0.1.1":
92
- self.report("SADDLE_VERSION", f'{self.manifest}: 必须为 saddle-framework = "=0.1.1"')
93
- elif name == "serde":
94
- valid = (
95
- isinstance(spec, dict)
96
- and spec.get("version") == "1"
97
- and set(spec) <= {"version", "features"}
98
- and spec.get("features") == ["derive"]
99
- )
100
- if not valid:
101
- self.report("DEPENDENCY", f'{self.manifest}: serde 仅允许 version="1", features=["derive"]')
102
-
103
- if "saddle-framework" not in dependencies:
104
- self.report("SADDLE_VERSION", f'{self.manifest}: 必须直接依赖 saddle-framework = "=0.1.1"')
105
-
106
- def check_layout(self) -> list[Path]:
107
- main = self.root / "src" / "main.rs"
108
- if not main.is_file():
109
- self.report("ENTRY", f"{main}: 必须使用默认二进制入口 src/main.rs")
110
-
111
- forbidden = [self.root / "build.rs", self.root / "src" / "lib.rs"]
112
- forbidden.extend((self.root / "src" / "bin").glob("*.rs") if (self.root / "src" / "bin").is_dir() else [])
113
- for path in forbidden:
114
- if path.exists():
115
- self.report("TARGET", f"{path}: 不允许额外 target 或 build script")
116
-
117
- if (self.root / ".cargo").exists():
118
- self.report("DEPENDENCY", f"{self.root / '.cargo'}: 不允许项目级 Cargo source/config 覆盖")
119
-
120
- source_dir = self.root / "src"
121
- return sorted(source_dir.rglob("*.rs")) if source_dir.is_dir() else []
122
-
123
- def check_sources(self, sources: list[Path]) -> None:
124
- stripped: dict[Path, str] = {}
125
- for path in sources:
126
- text = strip_rust_non_code(path.read_text(encoding="utf-8"))
127
- stripped[path] = text
128
- self.check_closed_source(path, text)
129
- for rule, patterns in SOURCE_RULES.items():
130
- if any(re.search(pattern, text) for pattern in patterns):
131
- self.report(rule, f"{path}: 命中禁止能力")
132
-
133
- main_count = sum(len(re.findall(r"\bfn\s+main\s*\(", text)) for text in stripped.values())
134
- if main_count != 1:
135
- self.report("ENTRY", f"{self.root / 'src'}: 必须且只能定义一个 fn main,实际为 {main_count}")
136
-
137
- main_path = self.root / "src" / "main.rs"
138
- main_text = stripped.get(main_path, "")
139
- body = function_body(main_text, "main")
140
- normalized_main = re.sub(r"\s+", "", main_text)
141
- if "use::saddle::Saddle;" not in normalized_main:
142
- self.report("ENTRY", f"{main_path}: 必须以 use ::saddle::Saddle 绑定发布 facade")
143
- saddle_occurrences = sum(
144
- len(re.findall(r"\bSaddle\b", source_text))
145
- for source_text in stripped.values()
146
- )
147
- if saddle_occurrences != 2:
148
- self.report("ENTRY", f"{self.root / 'src'}: Saddle 标识只能用于 facade 导入和唯一启动调用")
149
- if body is not None and re.search(r"\buse\s+", body):
150
- self.report("ENTRY", f"{main_path}: fn main/块作用域内禁止 use 与名称遮蔽")
151
- if body is None or len(re.findall(r"\bSaddle\s*::\s*run\s*\(", body)) != 1:
152
- self.report("ENTRY", f"{main_path}: fn main 函数体必须调用 Saddle::run")
153
- elif not call_is_tail_expression(body, "Saddle", "run"):
154
- self.report("ENTRY", f"{main_path}: Saddle::run 必须是 fn main 的唯一尾部启动调用")
155
-
156
- def check_closed_source(self, path: Path, text: str) -> None:
157
- if re.search(r"\bmacro_rules\s*!|\bmacro\s+[A-Za-z_]", text):
158
- self.report("MACRO", f"{path}: 禁止业务定义声明式或过程宏")
159
- for macro_name in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*!\s*[\(\{\[]", text):
160
- self.report("MACRO", f"{path}: 未批准的宏调用 {macro_name}!")
161
-
162
- for attribute in re.findall(r"#\s*\[\s*([^\]]+)\]", text, re.S):
163
- compact = re.sub(r"\s+", "", attribute)
164
- derive = re.fullmatch(r"derive\(([^)]+)\)", compact)
165
- names = set(derive.group(1).split(",")) if derive else set()
166
- if not derive or not names or not names <= {"Deserialize", "Serialize"}:
167
- self.report("ATTRIBUTE", f"{path}: 仅允许 serde Deserialize/Serialize derive")
168
-
169
- if re.search(r"\b(?:include|include_str|include_bytes)\s*!", text) or re.search(r"#\s*\[\s*path\s*=", text):
170
- self.report("SOURCE", f"{path}: 禁止 include!/外部 path 源码注入")
171
- if re.search(r"\bextern\s+crate\b", text):
172
- self.report("SOURCE", f"{path}: 禁止 extern crate/别名注入")
173
- if re.search(r"\bunsafe\b|\bextern\s*\"|\b(?:global_asm|asm)\s*!|#\s*\[\s*(?:link|no_mangle)", text):
174
- self.report("SOURCE", f"{path}: 禁止 unsafe、FFI、汇编或链接注入")
175
-
176
- for statement in re.findall(r"\buse\s+[^;]+;", text, re.S):
177
- compact = re.sub(r"\s+", "", statement)
178
- if re.search(r"\bas\b", statement):
179
- self.report("SOURCE", f"{path}: use 不允许 as 别名")
180
- if compact.startswith("use::std::"):
181
- if compact != "use::std::net::{IpAddr,Ipv4Addr,SocketAddr};":
182
- self.report("IO", f"{path}: 未批准的 ::std 导入")
183
- elif compact.startswith(("use::saddle::", "use::serde::")):
184
- pass
185
- elif compact.startswith(("usecrate::", "useself::", "usesuper::")):
186
- pass
187
- elif compact.startswith(("usestd::", "usesaddle::", "useserde::")):
188
- self.report("SOURCE", f"{path}: 外部 crate 必须使用不可遮蔽的绝对路径")
189
-
190
- allowed_std = re.sub(
191
- r"use\s*::\s*std\s*::\s*net\s*::\s*\{\s*IpAddr\s*,\s*Ipv4Addr\s*,\s*SocketAddr\s*\}\s*;",
192
- "",
193
- text,
194
- )
195
- allowed_std = re.sub(r"::\s*std\s*::\s*env\s*::\s*var\b", "", allowed_std)
196
- if re.search(r"(?<!:)\bstd\s*::|::\s*std\s*::", allowed_std):
197
- self.report("IO", f"{path}: 仅允许约定的 std 地址类型和部署环境变量读取")
198
- if re.search(r"(?<!:)\b(?:saddle|serde)\s*::", text):
199
- self.report("SOURCE", f"{path}: saddle/serde 必须从绝对 external-prelude 路径导入")
200
- for root in re.findall(r"(?<![:\w])::\s*([A-Za-z_][A-Za-z0-9_]*)\s*::", text):
201
- if root not in {"saddle", "serde", "std"}:
202
- self.report("SOURCE", f"{path}: 未批准的绝对 crate 路径 ::{root}")
203
-
204
- def check_compiler(self) -> None:
205
- lock = self.root / "Cargo.lock"
206
- if not lock.is_file():
207
- self.report("COMPILER", f"{lock}: 缺少锁文件,无法验证真实依赖解析")
208
- return
209
- with tempfile.TemporaryDirectory(prefix="saddle-gate-") as target:
210
- environment = os.environ.copy()
211
- environment["CARGO_TARGET_DIR"] = target
212
- result = subprocess.run(
213
- ["cargo", "check", "--locked", "--quiet", "--manifest-path", str(self.manifest)],
214
- env=environment,
215
- capture_output=True,
216
- text=True,
217
- check=False,
218
- )
219
- if result.returncode:
220
- detail = result.stderr.strip().splitlines()[-1] if result.stderr.strip() else "cargo check 失败"
221
- self.report("COMPILER", f"{self.manifest}: {detail}")
222
-
223
-
224
- def strip_rust_non_code(text: str) -> str:
225
- """Replace comments and literals with spaces while preserving newlines."""
226
- out = list(text)
227
- i = 0
228
- block_depth = 0
229
- while i < len(text):
230
- if block_depth:
231
- if text.startswith("/*", i):
232
- out[i:i + 2] = " "
233
- block_depth += 1
234
- i += 2
235
- elif text.startswith("*/", i):
236
- out[i:i + 2] = " "
237
- block_depth -= 1
238
- i += 2
239
- else:
240
- if text[i] != "\n":
241
- out[i] = " "
242
- i += 1
243
- continue
244
- if text.startswith("//", i):
245
- end = text.find("\n", i)
246
- end = len(text) if end < 0 else end
247
- out[i:end] = " " * (end - i)
248
- i = end
249
- continue
250
- if text.startswith("/*", i):
251
- out[i:i + 2] = " "
252
- block_depth = 1
253
- i += 2
254
- continue
255
- raw = re.match(r"(?:b)?r(#{0,255})\"", text[i:])
256
- if raw:
257
- marker = '"' + raw.group(1)
258
- start = i
259
- i += raw.end()
260
- end = text.find(marker, i)
261
- i = len(text) if end < 0 else end + len(marker)
262
- for j in range(start, i):
263
- if text[j] != "\n":
264
- out[j] = " "
265
- continue
266
- char = re.match(r"(?:b)?'(?:\\(?:u\{[0-9A-Fa-f_]+\}|.)|[^\\'\n])'", text[i:])
267
- if char:
268
- end = i + char.end()
269
- out[i:end] = " " * (end - i)
270
- i = end
271
- continue
272
- prefix = 1 if text.startswith('b"', i) else 0
273
- quote_at = i + prefix
274
- if quote_at < len(text) and text[quote_at] == '"':
275
- quote = '"'
276
- start = i
277
- i = quote_at + 1
278
- while i < len(text):
279
- if text[i] == "\\":
280
- i += 2
281
- elif text[i] == quote:
282
- i += 1
283
- break
284
- else:
285
- i += 1
286
- for j in range(start, min(i, len(text))):
287
- if text[j] != "\n":
288
- out[j] = " "
289
- continue
290
- i += 1
291
- return "".join(out)
292
-
293
-
294
- def function_body(text: str, name: str) -> str | None:
295
- match = re.search(rf"\bfn\s+{re.escape(name)}\s*\([^)]*\)[^{{;]*\{{", text, re.S)
296
- if not match:
297
- return None
298
- start = match.end() - 1
299
- depth = 0
300
- for index in range(start, len(text)):
301
- if text[index] == "{":
302
- depth += 1
303
- elif text[index] == "}":
304
- depth -= 1
305
- if depth == 0:
306
- return text[start + 1:index]
307
- return None
308
-
309
-
310
- def call_is_tail_expression(body: str, type_name: str, method: str) -> bool:
311
- match = re.search(
312
- rf"\b{re.escape(type_name)}\s*::\s*{re.escape(method)}\s*\(",
313
- body,
314
- )
315
- if not match:
316
- return False
317
- opening = body.find("(", match.start())
318
- depth = 0
319
- for index in range(opening, len(body)):
320
- if body[index] == "(":
321
- depth += 1
322
- elif body[index] == ")":
323
- depth -= 1
324
- if depth == 0:
325
- return not body[index + 1:].strip()
326
- return False
327
-
328
-
329
- def main() -> int:
330
- if len(sys.argv) != 2:
331
- print(f"用法: {sys.argv[0]} <业务 Cargo.toml>", file=sys.stderr)
332
- return 2
333
- manifest = Path(sys.argv[1])
334
- if not manifest.is_file():
335
- print(f"SADDLE_GATE MANIFEST {manifest}: 文件不存在", file=sys.stderr)
336
- return 2
337
- try:
338
- data = tomllib.loads(manifest.read_text(encoding="utf-8"))
339
- except (OSError, UnicodeError, tomllib.TOMLDecodeError) as error:
340
- print(f"SADDLE_GATE MANIFEST {manifest}: {error}", file=sys.stderr)
341
- return 2
342
-
343
- gate = Gate(manifest)
344
- gate.check_manifest(data)
345
- gate.check_sources(gate.check_layout())
346
- if not gate.failures:
347
- gate.check_compiler()
348
- if gate.failures:
349
- print(f"SADDLE_GATE rejected: {gate.failures} violation(s)", file=sys.stderr)
350
- return 1
351
- print(f"SADDLE_GATE passed: {gate.manifest}")
352
- return 0
353
-
354
-
355
- if __name__ == "__main__":
356
- raise SystemExit(main())