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

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 (40) hide show
  1. package/SKILL.md +62 -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 +24 -0
  9. package/package-manifest.json +111 -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 +48 -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 +123 -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 +86 -0
  22. package/scripts/validate-identity-closure.py +137 -0
  23. package/scripts/validate-machine-input.sh +48 -0
  24. package/scripts/validate-managed-objects-peak.py +147 -0
  25. package/LICENSE-APACHE +0 -203
  26. package/LICENSE-MIT +0 -22
  27. package/assets/v1-business/Cargo.toml +0 -12
  28. package/assets/v1-business/schema.sql +0 -18
  29. package/assets/v1-business/src/main.rs +0 -42
  30. package/assets/v1-business/src/order.rs +0 -118
  31. package/assets/v1-business/src/user.rs +0 -54
  32. package/references/db-0.1.1.md +0 -96
  33. package/references/gates-0.1.1.md +0 -77
  34. package/references/observability-0.1.1.md +0 -69
  35. package/references/overview-0.1.1.md +0 -73
  36. package/references/runtime-0.1.1.md +0 -54
  37. package/references/service-0.1.1.md +0 -124
  38. package/references/validation-0.1.1.md +0 -50
  39. package/scripts/check-business-boundaries.sh +0 -5
  40. package/scripts/check_business_boundaries.py +0 -356
@@ -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())