@antprofuse/saddle-skill 0.3.9 → 0.3.11
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/SKILL.md +14 -7
- package/agents/openai.yaml +1 -1
- package/assets/business-example/Cargo.lock +2606 -0
- package/assets/business-example/Cargo.toml +11 -0
- package/assets/business-example/contracts/api.proto +17 -0
- package/assets/business-example/contracts/saddle_external_function.proto +13 -0
- package/assets/business-example/mappings/users.json +1 -0
- package/assets/business-example/saddle.toml +31 -0
- package/assets/business-example/src/main.rs +389 -0
- package/package.json +2 -2
- package/references/business-example.md +40 -0
- package/references/consumer-start.md +2 -2
- package/references/database-name-mapping.md +47 -2
- package/references/external-function-contracts.md +2 -2
- package/references/local-integration.md +9 -6
- package/references/operations-observability.md +4 -3
- package/references/programming-model.md +21 -3
- package/references/response-model.md +2 -0
- package/references/validation.md +6 -4
- package/scripts/saddle-03-gate.js +7 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package puc;
|
|
4
|
+
|
|
5
|
+
import "saddle_external_function.proto";
|
|
6
|
+
|
|
7
|
+
service ExternalFunctions {
|
|
8
|
+
rpc Lookup(LookupRequest) returns (LookupResult) {
|
|
9
|
+
option (saddle.profusecontract.external_function) = {
|
|
10
|
+
puc: "puc"
|
|
11
|
+
function: "查询用户绑定户号"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
message LookupRequest { string user_id = 1; uint64 value = 2; }
|
|
17
|
+
message LookupResult { uint64 count = 1; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
package saddle.profusecontract;
|
|
3
|
+
|
|
4
|
+
import "google/protobuf/descriptor.proto";
|
|
5
|
+
|
|
6
|
+
message ExternalFunction {
|
|
7
|
+
string puc = 1;
|
|
8
|
+
string function = 2;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
extend google.protobuf.MethodOptions {
|
|
12
|
+
ExternalFunction external_function = 50001;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"table":{"from":"users","to":"app_users"},"columns":[{"from":"id","to":"user_id"},{"from":"value","to":"user_value"}]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[framework]
|
|
2
|
+
listen = "127.0.0.1:39101"
|
|
3
|
+
|
|
4
|
+
[framework.management]
|
|
5
|
+
bind = "127.0.0.1:39103"
|
|
6
|
+
|
|
7
|
+
[framework.lifecycle]
|
|
8
|
+
startTimeoutMs = 30000
|
|
9
|
+
shutdownTimeoutMs = 30000
|
|
10
|
+
|
|
11
|
+
[framework.admission]
|
|
12
|
+
cpuCores = 2
|
|
13
|
+
memoryMb = 512
|
|
14
|
+
requestTimeoutMs = 5000
|
|
15
|
+
|
|
16
|
+
[framework.admission.dependencies]
|
|
17
|
+
databaseConcurrency = 4
|
|
18
|
+
profusecontractConcurrency = 8
|
|
19
|
+
|
|
20
|
+
[framework.profusecontract]
|
|
21
|
+
authority = "http://127.0.0.1:39102"
|
|
22
|
+
|
|
23
|
+
[framework.observability.logging]
|
|
24
|
+
directory = "./logs"
|
|
25
|
+
rotation = "daily"
|
|
26
|
+
|
|
27
|
+
[secrets]
|
|
28
|
+
databaseUrlEnv = "SADDLE_DATABASE_URL"
|
|
29
|
+
|
|
30
|
+
[database]
|
|
31
|
+
mappingDir = "mappings"
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
use saddle::{
|
|
2
|
+
BusinessConfig, application, business_types,
|
|
3
|
+
ingress::{
|
|
4
|
+
FailureMessage, ProfuseGwCode, ProfuseGwContext, ProfuseGwFailure,
|
|
5
|
+
ProfuseGwResponse as Response,
|
|
6
|
+
},
|
|
7
|
+
profusecontract::ExternalFunctionResult,
|
|
8
|
+
testing::{
|
|
9
|
+
FakeExecutionCertainty, FakeProfuseContractBoundary, FakeStep, FakeTechnicalCode,
|
|
10
|
+
PROFUSEGW_MEDIA_TYPE, PROFUSEGW_METHOD, PROFUSEGW_PATH, ProfuseGwTestRequest,
|
|
11
|
+
TestIngressIdentity,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
business_types! {
|
|
16
|
+
#[derive(Clone, PartialEq, Message, Deserialize)]
|
|
17
|
+
#[serde(crate = "serde")]
|
|
18
|
+
struct IngressRequest {
|
|
19
|
+
#[prost(uint64, tag = "1")]
|
|
20
|
+
id: u64,
|
|
21
|
+
}
|
|
22
|
+
#[derive(Clone, PartialEq, Message, Serialize)]
|
|
23
|
+
#[serde(crate = "serde")]
|
|
24
|
+
struct IngressResponse {
|
|
25
|
+
#[prost(uint64, tag = "1")]
|
|
26
|
+
count: u64,
|
|
27
|
+
}
|
|
28
|
+
#[derive(Clone, PartialEq, Message)]
|
|
29
|
+
struct LookupRequest {
|
|
30
|
+
#[prost(string, tag = "1")]
|
|
31
|
+
user_id: String,
|
|
32
|
+
#[prost(uint64, tag = "2")]
|
|
33
|
+
value: u64,
|
|
34
|
+
}
|
|
35
|
+
#[derive(Clone, PartialEq, Message)]
|
|
36
|
+
struct LookupResult {
|
|
37
|
+
#[prost(uint64, tag = "1")]
|
|
38
|
+
count: u64,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
saddle::database_operations! {
|
|
43
|
+
pub mod generated {
|
|
44
|
+
table users { id: u64, value: u64 }
|
|
45
|
+
query_optional FindUser {
|
|
46
|
+
table users;
|
|
47
|
+
parameters FindUserParams { id => id }
|
|
48
|
+
result FindUserRow { value => value }
|
|
49
|
+
}
|
|
50
|
+
upsert SaveUser {
|
|
51
|
+
table users;
|
|
52
|
+
parameters SaveUserParams { id => id, value => value }
|
|
53
|
+
key id;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
enum Code {
|
|
59
|
+
Dependency,
|
|
60
|
+
}
|
|
61
|
+
impl ProfuseGwCode for Code {
|
|
62
|
+
const REGISTERED_CODES: &'static [&'static str] = &["TECHNICAL_DEPENDENCY"];
|
|
63
|
+
fn stable_code(&self) -> &'static str {
|
|
64
|
+
"TECHNICAL_DEPENDENCY"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async fn handler(
|
|
69
|
+
_request: IngressRequest,
|
|
70
|
+
context: ProfuseGwContext,
|
|
71
|
+
capabilities: Capabilities,
|
|
72
|
+
_business: BusinessConfig<()>,
|
|
73
|
+
) -> Response<IngressResponse, Code> {
|
|
74
|
+
assert_eq!(context.trace_info().trace_id(), "trace-1");
|
|
75
|
+
assert_eq!(context.trace_info().rpc_id(), "0");
|
|
76
|
+
assert_eq!(context.ldc_info().zone(), "z1");
|
|
77
|
+
assert_eq!(context.ldc_info().idc(), "i1");
|
|
78
|
+
assert_eq!(context.ldc_info().env(), "test");
|
|
79
|
+
match capabilities
|
|
80
|
+
.lookup(LookupRequest {
|
|
81
|
+
user_id: context.user_id().into(),
|
|
82
|
+
value: 0,
|
|
83
|
+
})
|
|
84
|
+
.await
|
|
85
|
+
{
|
|
86
|
+
ExternalFunctionResult::Completed(result) => Response::success(IngressResponse {
|
|
87
|
+
count: result.count,
|
|
88
|
+
}),
|
|
89
|
+
ExternalFunctionResult::TechnicalFailure(failure) => {
|
|
90
|
+
technical_failure(failure.code(), failure.certainty())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn technical_failure(
|
|
96
|
+
code: saddle::profusecontract::TechnicalFailureCode,
|
|
97
|
+
certainty: saddle::profusecontract::ExecutionCertainty,
|
|
98
|
+
) -> Response<IngressResponse, Code> {
|
|
99
|
+
use saddle::profusecontract::{ExecutionCertainty::*, TechnicalFailureCode::*};
|
|
100
|
+
let message = match (code, certainty) {
|
|
101
|
+
(FunctionNotFound, NotExecuted) => "FunctionNotFound: NotExecuted",
|
|
102
|
+
(FunctionNotFound, Executed) => "FunctionNotFound: Executed",
|
|
103
|
+
(FunctionNotFound, MayHaveExecuted) => "FunctionNotFound: MayHaveExecuted",
|
|
104
|
+
(FunctionRequestInvalid, NotExecuted) => "FunctionRequestInvalid: NotExecuted",
|
|
105
|
+
(FunctionRequestInvalid, Executed) => "FunctionRequestInvalid: Executed",
|
|
106
|
+
(FunctionRequestInvalid, MayHaveExecuted) => "FunctionRequestInvalid: MayHaveExecuted",
|
|
107
|
+
(CapacityRejected, NotExecuted) => "CapacityRejected: NotExecuted",
|
|
108
|
+
(CapacityRejected, Executed) => "CapacityRejected: Executed",
|
|
109
|
+
(CapacityRejected, MayHaveExecuted) => "CapacityRejected: MayHaveExecuted",
|
|
110
|
+
(DeadlineExceeded, NotExecuted) => "DeadlineExceeded: NotExecuted",
|
|
111
|
+
(DeadlineExceeded, Executed) => "DeadlineExceeded: Executed",
|
|
112
|
+
(DeadlineExceeded, MayHaveExecuted) => "DeadlineExceeded: MayHaveExecuted",
|
|
113
|
+
(DependencyUnavailable, NotExecuted) => "DependencyUnavailable: NotExecuted",
|
|
114
|
+
(DependencyUnavailable, Executed) => "DependencyUnavailable: Executed",
|
|
115
|
+
(DependencyUnavailable, MayHaveExecuted) => "DependencyUnavailable: MayHaveExecuted",
|
|
116
|
+
(ContractResultInvalid, NotExecuted) => "ContractResultInvalid: NotExecuted",
|
|
117
|
+
(ContractResultInvalid, Executed) => "ContractResultInvalid: Executed",
|
|
118
|
+
(ContractResultInvalid, MayHaveExecuted) => "ContractResultInvalid: MayHaveExecuted",
|
|
119
|
+
(InternalFailure, NotExecuted) => "InternalFailure: NotExecuted",
|
|
120
|
+
(InternalFailure, Executed) => "InternalFailure: Executed",
|
|
121
|
+
(InternalFailure, MayHaveExecuted) => "InternalFailure: MayHaveExecuted",
|
|
122
|
+
(TransportFailure, NotExecuted) => "TransportFailure: NotExecuted",
|
|
123
|
+
(TransportFailure, Executed) => "TransportFailure: Executed",
|
|
124
|
+
(TransportFailure, MayHaveExecuted) => "TransportFailure: MayHaveExecuted",
|
|
125
|
+
};
|
|
126
|
+
Response::failure(ProfuseGwFailure::new(
|
|
127
|
+
Code::Dependency,
|
|
128
|
+
FailureMessage::new(message).unwrap(),
|
|
129
|
+
))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn database_failure() -> Response<IngressResponse, Code> {
|
|
133
|
+
Response::failure(ProfuseGwFailure::new(
|
|
134
|
+
Code::Dependency,
|
|
135
|
+
FailureMessage::new("Database unavailable").unwrap(),
|
|
136
|
+
))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async fn query_handler(
|
|
140
|
+
request: IngressRequest,
|
|
141
|
+
context: ProfuseGwContext,
|
|
142
|
+
capabilities: Capabilities<QueryRoute>,
|
|
143
|
+
_: BusinessConfig<()>,
|
|
144
|
+
) -> Response<IngressResponse, Code> {
|
|
145
|
+
let (result, capabilities) = capabilities
|
|
146
|
+
.query((generated::FindUserParams { id: request.id }).into_parameters())
|
|
147
|
+
.await;
|
|
148
|
+
match result {
|
|
149
|
+
Ok(row) => match generated::FindUserRow::from_database(row) {
|
|
150
|
+
Some(row) => match capabilities
|
|
151
|
+
.lookup(LookupRequest {
|
|
152
|
+
user_id: context.user_id().into(),
|
|
153
|
+
value: row.value,
|
|
154
|
+
})
|
|
155
|
+
.await
|
|
156
|
+
{
|
|
157
|
+
ExternalFunctionResult::Completed(output) => Response::success(IngressResponse {
|
|
158
|
+
count: output.count,
|
|
159
|
+
}),
|
|
160
|
+
ExternalFunctionResult::TechnicalFailure(failure) => {
|
|
161
|
+
technical_failure(failure.code(), failure.certainty())
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
None => database_failure(),
|
|
165
|
+
},
|
|
166
|
+
Err(_) => database_failure(),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async fn write_handler(
|
|
171
|
+
request: IngressRequest,
|
|
172
|
+
_: ProfuseGwContext,
|
|
173
|
+
capabilities: Capabilities<WriteRoute>,
|
|
174
|
+
_: BusinessConfig<()>,
|
|
175
|
+
) -> Response<IngressResponse, Code> {
|
|
176
|
+
let (result, capabilities) = capabilities
|
|
177
|
+
.write(
|
|
178
|
+
(generated::SaveUserParams {
|
|
179
|
+
id: request.id,
|
|
180
|
+
value: 7,
|
|
181
|
+
})
|
|
182
|
+
.into_parameters(),
|
|
183
|
+
)
|
|
184
|
+
.await;
|
|
185
|
+
match result {
|
|
186
|
+
Ok(value) => Response::success(IngressResponse {
|
|
187
|
+
count: value.rows_affected(),
|
|
188
|
+
}),
|
|
189
|
+
Err(_) => database_failure(),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async fn transaction_handler(
|
|
194
|
+
request: IngressRequest,
|
|
195
|
+
_: ProfuseGwContext,
|
|
196
|
+
capabilities: Capabilities<TransactionRoute>,
|
|
197
|
+
_: BusinessConfig<()>,
|
|
198
|
+
) -> Response<IngressResponse, Code> {
|
|
199
|
+
let (result, capabilities) = capabilities
|
|
200
|
+
.transaction(
|
|
201
|
+
(generated::SaveUserParams {
|
|
202
|
+
id: request.id,
|
|
203
|
+
value: 9,
|
|
204
|
+
})
|
|
205
|
+
.into_parameters(),
|
|
206
|
+
)
|
|
207
|
+
.await;
|
|
208
|
+
match result {
|
|
209
|
+
Ok(value) => Response::success(IngressResponse {
|
|
210
|
+
count: value.rows_affected(),
|
|
211
|
+
}),
|
|
212
|
+
Err(_) => database_failure(),
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async fn rollback_handler(
|
|
217
|
+
request: IngressRequest,
|
|
218
|
+
_: ProfuseGwContext,
|
|
219
|
+
capabilities: Capabilities<RollbackRoute>,
|
|
220
|
+
_: BusinessConfig<()>,
|
|
221
|
+
) -> Response<IngressResponse, Code> {
|
|
222
|
+
let (result, capabilities) = capabilities
|
|
223
|
+
.rollback(
|
|
224
|
+
(generated::SaveUserParams {
|
|
225
|
+
id: request.id,
|
|
226
|
+
value: 11,
|
|
227
|
+
})
|
|
228
|
+
.into_parameters(),
|
|
229
|
+
)
|
|
230
|
+
.await;
|
|
231
|
+
match result {
|
|
232
|
+
Ok(value) => Response::success(IngressResponse {
|
|
233
|
+
count: value.rows_affected(),
|
|
234
|
+
}),
|
|
235
|
+
Err(_) => database_failure(),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
application! {
|
|
240
|
+
schema "saddle-application/2";
|
|
241
|
+
application BusinessApp;
|
|
242
|
+
deployment_app "business-app";
|
|
243
|
+
business_config ();
|
|
244
|
+
profusecontract {
|
|
245
|
+
contract_dir "contracts";
|
|
246
|
+
capability Capabilities;
|
|
247
|
+
response_code Code;
|
|
248
|
+
functions {
|
|
249
|
+
Lookup => lookup {
|
|
250
|
+
business_unit "puc";
|
|
251
|
+
function "查询用户绑定户号";
|
|
252
|
+
} (LookupRequest) -> LookupResult;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
service LookupIngress {
|
|
256
|
+
ingress profusegw;
|
|
257
|
+
operation_type "business.lookup";
|
|
258
|
+
request IngressRequest;
|
|
259
|
+
response IngressResponse;
|
|
260
|
+
handler handler;
|
|
261
|
+
uses Lookup;
|
|
262
|
+
}
|
|
263
|
+
service QueryRoute { ingress profusegw; operation_type "business.query"; request IngressRequest; response IngressResponse; handler query_handler; uses Lookup; database query_optional query (generated::FindUser); }
|
|
264
|
+
service WriteRoute { ingress profusegw; operation_type "business.write"; request IngressRequest; response IngressResponse; handler write_handler; uses Lookup; database write write (generated::SaveUser); }
|
|
265
|
+
service TransactionRoute { ingress profusegw; operation_type "business.transaction"; request IngressRequest; response IngressResponse; handler transaction_handler; uses Lookup; database transaction_commit transaction (generated::SaveUser); }
|
|
266
|
+
service RollbackRoute { ingress profusegw; operation_type "business.rollback"; request IngressRequest; response IngressResponse; handler rollback_handler; uses Lookup; database transaction_rollback rollback (generated::SaveUser); }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn identity(name: &str) -> TestIngressIdentity {
|
|
270
|
+
TestIngressIdentity::new(name, "incoming", 1_800_000_000_000).unwrap()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn request(method: &str, path: &str, media_type: &str) -> ProfuseGwTestRequest {
|
|
274
|
+
ProfuseGwTestRequest::new(
|
|
275
|
+
method,
|
|
276
|
+
path,
|
|
277
|
+
media_type,
|
|
278
|
+
identity("request-1"),
|
|
279
|
+
r#"{
|
|
280
|
+
"target":{"app":"business-app","interfaceId":"business.lookup"},
|
|
281
|
+
"profuseGwContext":{"userInfo":{"userId":"2088-user"},"traceInfo":{"traceId":"trace-1","rpcId":"0"},"ldcInfo":{"zone":"z1","idc":"i1","env":"test"}},
|
|
282
|
+
"requestData":{"id":1}
|
|
283
|
+
}"#,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn smoke() {
|
|
288
|
+
let execution = BusinessApp::profusegw_test_harness(
|
|
289
|
+
FakeProfuseContractBoundary::completed(LookupResult { count: 7 }),
|
|
290
|
+
BusinessConfig::unit(),
|
|
291
|
+
)
|
|
292
|
+
.run(request(
|
|
293
|
+
PROFUSEGW_METHOD,
|
|
294
|
+
PROFUSEGW_PATH,
|
|
295
|
+
PROFUSEGW_MEDIA_TYPE,
|
|
296
|
+
))
|
|
297
|
+
.unwrap();
|
|
298
|
+
assert_eq!(
|
|
299
|
+
execution.response_json().unwrap(),
|
|
300
|
+
r#"{"success":true,"data":{"count":7}}"#
|
|
301
|
+
);
|
|
302
|
+
let attempt = &execution.attempts()[0];
|
|
303
|
+
assert_eq!(attempt.request_id(), "request-1");
|
|
304
|
+
assert_eq!(attempt.call_id(), "incoming-1");
|
|
305
|
+
assert_eq!(attempt.trace_id(), "trace-1");
|
|
306
|
+
assert_eq!(attempt.rpc_id(), "0.1");
|
|
307
|
+
assert_eq!(attempt.zone(), "z1");
|
|
308
|
+
assert_eq!(attempt.idc(), "i1");
|
|
309
|
+
assert_eq!(attempt.env(), "test");
|
|
310
|
+
assert_eq!(attempt.function(), "查询用户绑定户号");
|
|
311
|
+
|
|
312
|
+
for (method, path, media_type, expected) in [
|
|
313
|
+
(
|
|
314
|
+
"GET",
|
|
315
|
+
PROFUSEGW_PATH,
|
|
316
|
+
PROFUSEGW_MEDIA_TYPE,
|
|
317
|
+
"METHOD_NOT_ALLOWED",
|
|
318
|
+
),
|
|
319
|
+
(
|
|
320
|
+
PROFUSEGW_METHOD,
|
|
321
|
+
"/wrong",
|
|
322
|
+
PROFUSEGW_MEDIA_TYPE,
|
|
323
|
+
"PATH_NOT_FOUND",
|
|
324
|
+
),
|
|
325
|
+
(
|
|
326
|
+
PROFUSEGW_METHOD,
|
|
327
|
+
PROFUSEGW_PATH,
|
|
328
|
+
"text/plain",
|
|
329
|
+
"MEDIA_TYPE_NOT_SUPPORTED",
|
|
330
|
+
),
|
|
331
|
+
] {
|
|
332
|
+
let result = BusinessApp::profusegw_test_harness(
|
|
333
|
+
FakeProfuseContractBoundary::completed(LookupResult { count: 0 }),
|
|
334
|
+
BusinessConfig::unit(),
|
|
335
|
+
)
|
|
336
|
+
.run(request(method, path, media_type));
|
|
337
|
+
let error = match result {
|
|
338
|
+
Err(error) => error,
|
|
339
|
+
Ok(_) => panic!("invalid framing must be rejected"),
|
|
340
|
+
};
|
|
341
|
+
assert_eq!(error.code(), expected);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let technical_codes = [
|
|
345
|
+
FakeTechnicalCode::FunctionNotFound,
|
|
346
|
+
FakeTechnicalCode::FunctionRequestInvalid,
|
|
347
|
+
FakeTechnicalCode::CapacityRejected,
|
|
348
|
+
FakeTechnicalCode::DeadlineExceeded,
|
|
349
|
+
FakeTechnicalCode::DependencyUnavailable,
|
|
350
|
+
FakeTechnicalCode::ContractResultInvalid,
|
|
351
|
+
FakeTechnicalCode::InternalFailure,
|
|
352
|
+
FakeTechnicalCode::TransportFailure,
|
|
353
|
+
];
|
|
354
|
+
let certainties = [
|
|
355
|
+
FakeExecutionCertainty::NotExecuted,
|
|
356
|
+
FakeExecutionCertainty::Executed,
|
|
357
|
+
FakeExecutionCertainty::MayHaveExecuted,
|
|
358
|
+
];
|
|
359
|
+
for code in technical_codes {
|
|
360
|
+
for certainty in certainties {
|
|
361
|
+
let technical = BusinessApp::profusegw_test_harness(
|
|
362
|
+
FakeProfuseContractBoundary::scripted([FakeStep::technical_failure(
|
|
363
|
+
code, certainty,
|
|
364
|
+
)]),
|
|
365
|
+
BusinessConfig::unit(),
|
|
366
|
+
)
|
|
367
|
+
.run(request(
|
|
368
|
+
PROFUSEGW_METHOD,
|
|
369
|
+
PROFUSEGW_PATH,
|
|
370
|
+
PROFUSEGW_MEDIA_TYPE,
|
|
371
|
+
))
|
|
372
|
+
.unwrap();
|
|
373
|
+
assert!(
|
|
374
|
+
matches!(technical.response(), ProfuseGwResponse::LookupIngress(response) if response.failure_value().is_some())
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fn main() {
|
|
381
|
+
if std::env::args().nth(1).as_deref() == Some("--smoke") {
|
|
382
|
+
smoke();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if let Err(error) = BusinessApp::run() {
|
|
386
|
+
eprintln!("{}", error.code());
|
|
387
|
+
std::process::exit(1);
|
|
388
|
+
}
|
|
389
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antprofuse/saddle-skill",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Saddle 0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
|
+
"description": "Saddle 0.3.11 中文 AI Coding 研发契约与业务门禁。",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"files": ["SKILL.md", "agents", "references", "scripts", "assets"],
|
|
7
7
|
"bin": {"saddle-03-gate": "scripts/saddle-03-gate.js"},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 从已安装 Skill 启动完整业务示例
|
|
2
|
+
|
|
3
|
+
本包包含 `assets/business-example` 完整工程:Cargo.toml、lock、Rust业务源码、固定协议与业务proto、saddle.toml、表映射。业务唯一直接依赖是 `saddle-framework = "=0.3.11"`;不添加内部crate或patch。
|
|
4
|
+
|
|
5
|
+
0.3.11 未发布时只能使用发布方提供的同版本候选 registry 和依赖缓存。候选烟测不证明公网可安装;版本未就绪应停止,不能回退旧版或把本地源码当registry。发布后再执行公网 exact 安装。示例的候选 lock 需在获批 registry 准备阶段由正常 Cargo 解析形成对应checksum lock,随后必须 locked/offline。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 候选阶段:SKILL_TGZ 指向发布方提供的精确tgz,先核对其SHA256。
|
|
9
|
+
npm install --save-exact --ignore-scripts --no-audit --no-fund "$SKILL_TGZ"
|
|
10
|
+
cp -R node_modules/@antprofuse/saddle-skill/assets/business-example ./business-app
|
|
11
|
+
cd business-app
|
|
12
|
+
# 仅依赖准备阶段允许解析;候选registry由发布方提供,勿手写patch。
|
|
13
|
+
cargo +1.95.0 generate-lockfile --offline
|
|
14
|
+
../node_modules/.bin/saddle-03-gate --manifest-path Cargo.toml
|
|
15
|
+
cargo +1.95.0 run --locked --offline -- --smoke
|
|
16
|
+
cargo +1.95.0 build --release --locked --offline
|
|
17
|
+
target/release/saddle-0311-consumer --config /absolute/path/business-app/saddle.toml
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`--smoke` 是该示例的受控无DB外呼测试;默认启动路径调用真正的 BusinessApp::run。完整 DB→网络外呼需要以下部署输入,不会由 smoke 伪造:
|
|
21
|
+
|
|
22
|
+
- 预先存在的 MariaDB app_users 表,user_id/user_value 为规范非空整数,user_id 为唯一键;DDL由业务运维负责。
|
|
23
|
+
- 显式 `SADDLE_DATABASE_URL` secret 环境变量;不要把值写入配置、命令记录或日志。
|
|
24
|
+
- ProfuseContract 实现本目录 api.proto 的“查询用户绑定户号”,接收 handler 显式传入的user_id和数据库value,返回count。将 `[framework.profusecontract].authority` 改成真实plaintext地址。
|
|
25
|
+
- 根据环境选择业务listen、独立management bind及日志目录。其余配置规则见 [启动配置](local-integration.md)。本示例 `business_config ();` 不需要[business]。
|
|
26
|
+
|
|
27
|
+
同一固定入口的调用示例(先write,再query):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
curl -i -X POST http://127.0.0.1:39101/saddle/v1/ingress/profusegw/invoke \
|
|
31
|
+
-H 'Content-Type: application/json' \
|
|
32
|
+
--data '{"target":{"app":"business-app","interfaceId":"business.write"},"profuseGwContext":{"userInfo":{"userId":"2088-user"},"traceInfo":{"traceId":"trace-1","rpcId":"0"},"ldcInfo":{"zone":"z1","idc":"i1","env":"test"}},"requestData":{"id":1}}'
|
|
33
|
+
curl -i -X POST http://127.0.0.1:39101/saddle/v1/ingress/profusegw/invoke \
|
|
34
|
+
-H 'Content-Type: application/json' \
|
|
35
|
+
--data '{"target":{"app":"business-app","interfaceId":"business.query"},"profuseGwContext":{"userInfo":{"userId":"2088-user"},"traceInfo":{"traceId":"trace-1","rpcId":"0"},"ldcInfo":{"zone":"z1","idc":"i1","env":"test"}},"requestData":{"id":1}}'
|
|
36
|
+
curl -i http://127.0.0.1:39103/ready
|
|
37
|
+
curl -i http://127.0.0.1:39103/metrics
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
business.query 的query结果进入同请求真实外呼,再形成框架响应。business.transaction 与 business.rollback 分别是静态commit/rollback route;不能把它们外推成handler控制的动态事务。超大响应受现有1MiB编码与framing边界约束,超界是技术失败,不截断成功。已知DB贡献使用现有memory配置推导并发;不承诺RSS/driver/TLS/任意业务分配绝对硬界。
|
|
@@ -46,7 +46,7 @@ Saddle 在编译期从 `contract_dir` 确定性递归收集 `.proto`,闭合 im
|
|
|
46
46
|
|
|
47
47
|
## 阶段 3:Rust 实现
|
|
48
48
|
|
|
49
|
-
业务 crate 精确使用 `saddle-framework = "=0.3.
|
|
49
|
+
业务 crate 精确使用 `saddle-framework = "=0.3.11"`,然后按 [最小编程模型](programming-model.md) 编写固定 `deployment_app`、显式 `business_config`、静态 operation 声明和四参数 handler。不得直接依赖任何 Saddle 中间件 crate。
|
|
50
50
|
|
|
51
51
|
实现通过 Gate 后,按 [本地联调](local-integration.md) 由外部 harness 注入唯一 plaintext profusecontract authority,并以固定 profusegw body 验证真实 typed call。endpoint、request/call identity 和 deadline 都是框架或部署输入,不写入业务 spec。
|
|
52
52
|
|
|
@@ -63,6 +63,6 @@ Saddle 在编译期从 `contract_dir` 确定性递归收集 `.proto`,闭合 im
|
|
|
63
63
|
- 缺少固定 `deployment_app`,或 app/interfaceId/endpoint 被做成动态业务输入;
|
|
64
64
|
- 技术失败码或执行确定性存在 catch-all/default;
|
|
65
65
|
- 需要裸 gRPC/channel/bytes、第二入口、动态 descriptor 或未声明外部 I/O;
|
|
66
|
-
- `saddle-framework` 不是精确 `=0.3.
|
|
66
|
+
- `saddle-framework` 不是精确 `=0.3.11`,或 [正式 Gate](validation.md) 失败。
|
|
67
67
|
|
|
68
68
|
停止产物只包含最小复现、缺失身份和期望业务表达,不包含中间件替代实现。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 数据库名称映射
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
业务源码用公开 `saddle::database_operations!` 声明逻辑表、逻辑字段和静态 operation。正常 Cargo build 生成 typed operation、具名 Params/Row 及启动注册,不需要独立生成器、build.rs、SQL 或隐藏 trait。框架自动完成:
|
|
4
4
|
|
|
5
|
-
1.
|
|
5
|
+
1. 从 saddle.toml 的 `[database].mappingDir` 读取部署映射目录;
|
|
6
6
|
2. 注册应用实际使用的全部逻辑表;
|
|
7
7
|
3. 注册应用实际使用的全部 query 与 write/transaction 操作。
|
|
8
8
|
|
|
@@ -11,3 +11,48 @@
|
|
|
11
11
|
业务 Agent 不直接调用隐藏的注册 API;应由 Saddle 生成代码为声明过的静态表和操作生成接线。数据库关闭时不得声明映射目录或数据库操作。映射只改变部署名称,不改变业务类型、SQL 语义、事务边界或响应模型。
|
|
12
12
|
|
|
13
13
|
本地和正式 Gate 至少覆盖:映射 query、映射 write、transaction commit、业务失败 rollback,以及缺失/漂移映射 fail-closed。任何路径不得把物理库表名暴露为业务配置或请求字段。
|
|
14
|
+
|
|
15
|
+
## 最小公开声明
|
|
16
|
+
|
|
17
|
+
```rust
|
|
18
|
+
saddle::database_operations! {
|
|
19
|
+
pub mod ops {
|
|
20
|
+
table users { id: u64, value: u64 }
|
|
21
|
+
query_optional Read {
|
|
22
|
+
table users;
|
|
23
|
+
parameters ReadParams { id => id }
|
|
24
|
+
result ReadRow { value => value }
|
|
25
|
+
}
|
|
26
|
+
upsert Save {
|
|
27
|
+
table users;
|
|
28
|
+
parameters SaveParams { id => id, value => value }
|
|
29
|
+
key id;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
route 使用 `database query_optional read (ops::Read);` 或 `database write write (ops::Save);`。多个 route 可共享同一个 operation。静态 `transaction_commit` / `transaction_rollback` 也可引用 Save,不提供动态多步事务、嵌套或业务 commit/rollback。
|
|
36
|
+
|
|
37
|
+
调用 `let (result, c) = c.read(ops::ReadParams { id }.into_parameters()).await;`。成功时 `ops::ReadRow::from_database(row)` 得到 `Option<ops::ReadRow>`;Some 字段直接读取,None 仅表示没有匹配行。剩余 c 可 typed 外呼但没有第二次 DB 方法,同请求 context/deadline 保持。完整工程见 [业务示例](business-example.md)。
|
|
38
|
+
|
|
39
|
+
query_optional 固定 LIMIT 1,无 ORDER BY,不承诺多匹配行的选取顺序,不检测唯一性,也不限制服务器扫描工作。多行、nullable、String/Vec、浮点不是本版声明类型。
|
|
40
|
+
|
|
41
|
+
| 声明 | 合法物理表示与值 | 拒绝 |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| u64 | MariaDB整数或 DECIMAL(p,0),0..18446744073709551615 | 负数、溢出、NULL、数字文本、BIT、浮点、小数编码 |
|
|
44
|
+
| i64 | 整数或 DECIMAL(p,0),-9223372036854775808..9223372036854775807 | 溢出、NULL、数字文本、BIT、浮点、小数编码 |
|
|
45
|
+
| bool | 整数或 DECIMAL(p,0) 中的0/1,包括 BOOLEAN/TINYINT 的0/1 | 其他整数、NULL、BIT、文本 |
|
|
46
|
+
| bytes<N> | 二进制/字符列查询值完整原字节,长度不超过N,具名字段为 FixedDbBytes<N>,用 as_slice() 读取 | 超长或NULL明确 db.invalid_column,不截断、不伪装None |
|
|
47
|
+
|
|
48
|
+
CHAR 的服务器尾空格规则仍由数据库决定。非法结果须映射技术失败;不能伪造空成功。
|
|
49
|
+
|
|
50
|
+
映射每表一个JSON。例如 users.json:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{"table":{"from":"users","to":"app_users"},"columns":[{"from":"id","to":"user_id"},{"from":"value","to":"user_value"}]}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
物理表由部署环境预先准备,Saddle 不执行 DDL/迁移。连接只通过 `[secrets].databaseUrlEnv` 引用环境变量,普通配置不由环境变量覆盖。
|
|
57
|
+
|
|
58
|
+
已知 DB 参数、行、转换贡献从声明推导,以现有 memoryMb 计算可准入并发;不新增 DB memory、fieldBytes 或并发预算参数。不承诺全进程 RSS、driver/TLS、任意业务分配的绝对硬界。
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- `puc` 是业务单元,例如 `puc`。
|
|
16
16
|
- `function` 是契约原始函数名,允许中文,例如 `查询用户绑定户号`。
|
|
17
17
|
- protobuf method、Rust 方法和 Java 方法使用各语言合法的本地标识符;它们不是跨系统身份。
|
|
18
|
-
- 0.3.
|
|
18
|
+
- 0.3.11 不定义 contract version、多版本路由、翻译表、alias 或 fallback。
|
|
19
19
|
|
|
20
20
|
## 定义步骤
|
|
21
21
|
|
|
@@ -84,4 +84,4 @@ message BoundAccount {
|
|
|
84
84
|
|
|
85
85
|
## 变更规则
|
|
86
86
|
|
|
87
|
-
先修改共同契约目录并重新通过双方 conformance,再修改两侧实现。禁止两侧各自维护近似定义后人工对齐;0.3.
|
|
87
|
+
先修改共同契约目录并重新通过双方 conformance,再修改两侧实现。禁止两侧各自维护近似定义后人工对齐;0.3.11 遇到不兼容变化时停止并共同升级,不在运行时并存或猜测版本。
|