rubricloop 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {rubricloop-0.1.2 → rubricloop-0.1.4}/PKG-INFO +6 -5
- {rubricloop-0.1.2 → rubricloop-0.1.4}/README.md +5 -4
- rubricloop-0.1.4/examples/verify_reply_registry/README.md +65 -0
- rubricloop-0.1.4/examples/verify_reply_registry/build_package.py +122 -0
- rubricloop-0.1.4/examples/verify_reply_registry/run-input.json +6 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/pyproject.toml +1 -1
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/__init__.py +1 -1
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/cli.py +19 -1
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/__init__.py +2 -1
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/catalog.py +19 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/support.py +35 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/registry.py +1 -1
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_registry_cli.py +55 -1
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_wave_one_packs.py +27 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/.gitignore +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/LICENSE +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/RLPACK.md +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/VETTING.md +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/examples/sql_agent.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/examples/verify_reply.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/__init__.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/common.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/sql.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/checks/structured.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/examples/__init__.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/examples/verify_reply.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/loop.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/base.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/commerce.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/finance.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/logistics.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/official.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/refund.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/security.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/sql.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/packs/vetting.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/py.typed +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/rlpack.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/rubric.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/sandboxes/__init__.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/sandboxes/sqlite.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/src/rubricloop/types.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_common_checks.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_invariant.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_loop.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_refund_pack.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_rlpack.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_sql_pack.py +0 -0
- {rubricloop-0.1.2 → rubricloop-0.1.4}/tests/test_vetting_scenarios.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: rubricloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Bounded, auditable verification loops for AI agent output.
|
|
5
5
|
Project-URL: Homepage, https://rubricloop.com
|
|
6
6
|
Project-URL: Documentation, https://rubricloop.com/docs
|
|
@@ -179,10 +179,11 @@ When the package is ready to share, create an API key in the
|
|
|
179
179
|
hosted registry:
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
|
-
rubricloop
|
|
183
|
-
rubricloop
|
|
184
|
-
rubricloop
|
|
185
|
-
rubricloop
|
|
182
|
+
rubricloop login --api-key "$RUBRICLOOP_API_KEY"
|
|
183
|
+
rubricloop create acme/check --visibility private
|
|
184
|
+
rubricloop push dist/acme-check.rlpack --tag latest
|
|
185
|
+
rubricloop vet acme/check@latest
|
|
186
|
+
rubricloop pull acme/check@latest
|
|
186
187
|
```
|
|
187
188
|
|
|
188
189
|
Public community packages are free. Private repositories are available only to
|
|
@@ -150,10 +150,11 @@ When the package is ready to share, create an API key in the
|
|
|
150
150
|
hosted registry:
|
|
151
151
|
|
|
152
152
|
```bash
|
|
153
|
-
rubricloop
|
|
154
|
-
rubricloop
|
|
155
|
-
rubricloop
|
|
156
|
-
rubricloop
|
|
153
|
+
rubricloop login --api-key "$RUBRICLOOP_API_KEY"
|
|
154
|
+
rubricloop create acme/check --visibility private
|
|
155
|
+
rubricloop push dist/acme-check.rlpack --tag latest
|
|
156
|
+
rubricloop vet acme/check@latest
|
|
157
|
+
rubricloop pull acme/check@latest
|
|
157
158
|
```
|
|
158
159
|
|
|
159
160
|
Public community packages are free. Private repositories are available only to
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Reply Quick Start registry lifecycle
|
|
2
|
+
|
|
3
|
+
This package uses the same three deterministic rules as
|
|
4
|
+
`sdk/examples/verify_reply.py`: 40 to 70 words, the word `timeline`, and no
|
|
5
|
+
`guaranteed refund` promise.
|
|
6
|
+
|
|
7
|
+
You can run the same checks through Hosted Verification before publishing and
|
|
8
|
+
vetting your own package:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
curl https://api.rubricloop.dev/v1/verify \
|
|
12
|
+
-H "Content-Type: application/json" \
|
|
13
|
+
-H "X-API-Key: $RUBRICLOOP_API_KEY" \
|
|
14
|
+
-d '{
|
|
15
|
+
"pack_id": "support/reply-v1",
|
|
16
|
+
"output": {
|
|
17
|
+
"reply": "We apologize for the delay with your order. Our team is currently investigating the issue and working to resolve it as quickly as possible. You can expect an update on the timeline for your order shortly. Thank you for your patience."
|
|
18
|
+
},
|
|
19
|
+
"context": {}
|
|
20
|
+
}'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To publish the package itself:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install rubricloop openai
|
|
27
|
+
export OPENAI_API_KEY="sk-..."
|
|
28
|
+
python sdk/examples/verify_reply.py
|
|
29
|
+
|
|
30
|
+
python sdk/examples/verify_reply_registry/build_package.py \
|
|
31
|
+
--namespace YOUR_NAMESPACE
|
|
32
|
+
|
|
33
|
+
rubricloop login
|
|
34
|
+
rubricloop create \
|
|
35
|
+
YOUR_NAMESPACE/support-reply \
|
|
36
|
+
--visibility private \
|
|
37
|
+
--description "Checks support replies before they are sent."
|
|
38
|
+
rubricloop validate dist/support-reply.rlpack
|
|
39
|
+
rubricloop test dist/support-reply.rlpack
|
|
40
|
+
rubricloop push \
|
|
41
|
+
dist/support-reply.rlpack \
|
|
42
|
+
--tag latest
|
|
43
|
+
rubricloop vet \
|
|
44
|
+
YOUR_NAMESPACE/support-reply@latest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pull the immutable bundle digest returned by `push`, not the mutable tag:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
rubricloop pull \
|
|
51
|
+
YOUR_NAMESPACE/support-reply@sha256:BUNDLE_DIGEST \
|
|
52
|
+
--output dist/pulled-support-reply.rlpack
|
|
53
|
+
cmp dist/support-reply.rlpack dist/pulled-support-reply.rlpack
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Once vetting passes and hosted execution is approved for that exact digest:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
rubricloop run \
|
|
60
|
+
YOUR_NAMESPACE/support-reply@sha256:BUNDLE_DIGEST \
|
|
61
|
+
--input sdk/examples/verify_reply_registry/run-input.json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
No credential belongs in this folder. `login` reads the key without echoing it
|
|
65
|
+
and stores it in the operating system keychain.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Build the registry package used by the OpenAI reply Quick Start."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rubricloop import build_rlpack, create_manifest, read_rlpack
|
|
10
|
+
|
|
11
|
+
VALID_REPLY = (
|
|
12
|
+
"We apologize for the delay with your order. Our team is currently investigating "
|
|
13
|
+
"the issue and working to resolve it as quickly as possible. You can expect an "
|
|
14
|
+
"update on the timeline for your order shortly. Thank you for your patience."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def manifest(namespace: str, version: str) -> dict:
|
|
19
|
+
golden = [
|
|
20
|
+
{
|
|
21
|
+
"id": f"valid-reply-{index:02d}",
|
|
22
|
+
"input": {"reply": VALID_REPLY},
|
|
23
|
+
"context": {},
|
|
24
|
+
"expected": {"decision": "allow"},
|
|
25
|
+
}
|
|
26
|
+
for index in range(1, 21)
|
|
27
|
+
]
|
|
28
|
+
return create_manifest(
|
|
29
|
+
namespace=namespace,
|
|
30
|
+
name="support-reply",
|
|
31
|
+
version=version,
|
|
32
|
+
title="Support reply",
|
|
33
|
+
description="Checks reply length, timeline wording, and refund promises.",
|
|
34
|
+
license="MIT",
|
|
35
|
+
categories=("Support", "Safety"),
|
|
36
|
+
rules=(
|
|
37
|
+
{
|
|
38
|
+
"id": "reply.length",
|
|
39
|
+
"kind": "text.word_count",
|
|
40
|
+
"severity": "error",
|
|
41
|
+
"description": "Reply must contain 40 to 70 words.",
|
|
42
|
+
"config": {"field": "reply", "minimum": 40, "maximum": 70},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "reply.timeline",
|
|
46
|
+
"kind": "text.required_terms",
|
|
47
|
+
"severity": "error",
|
|
48
|
+
"description": "Reply must explain the timeline.",
|
|
49
|
+
"config": {"field": "reply", "terms": ["timeline"]},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "reply.promise",
|
|
53
|
+
"kind": "text.forbidden_terms",
|
|
54
|
+
"severity": "error",
|
|
55
|
+
"description": "Reply must not promise a guaranteed refund.",
|
|
56
|
+
"config": {"field": "reply", "terms": ["guaranteed refund"]},
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
tests={
|
|
60
|
+
"golden": golden,
|
|
61
|
+
"mutation": [
|
|
62
|
+
{
|
|
63
|
+
"id": "forbidden-refund-promise",
|
|
64
|
+
"input": {"reply": "We offer a guaranteed refund."},
|
|
65
|
+
"context": {},
|
|
66
|
+
"expected": {"decision": "deny"},
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"fail_closed": [
|
|
70
|
+
{
|
|
71
|
+
"id": "missing-reply",
|
|
72
|
+
"input": {},
|
|
73
|
+
"context": {},
|
|
74
|
+
"expected": {"decision": "deny"},
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
examples=(
|
|
79
|
+
{
|
|
80
|
+
"name": "delayed-order",
|
|
81
|
+
"input": {"reply": VALID_REPLY},
|
|
82
|
+
"context": {},
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
provenance={
|
|
86
|
+
"builder": "rubricloop-sdk",
|
|
87
|
+
"source_repository": "https://rubricloop.com/docs",
|
|
88
|
+
"source_revision": "verify-reply-quick-start",
|
|
89
|
+
"built_at": "",
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main() -> None:
|
|
95
|
+
parser = argparse.ArgumentParser()
|
|
96
|
+
parser.add_argument("--namespace", required=True)
|
|
97
|
+
parser.add_argument("--version", default="1.0.0")
|
|
98
|
+
parser.add_argument("--output", type=Path, default=Path("dist/support-reply.rlpack"))
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
bundle = build_rlpack(
|
|
102
|
+
manifest(args.namespace, args.version),
|
|
103
|
+
{
|
|
104
|
+
"payload/quick-start.json": json.dumps(
|
|
105
|
+
{
|
|
106
|
+
"example": "sdk/examples/verify_reply.py",
|
|
107
|
+
"model": "gpt-4o-mini",
|
|
108
|
+
},
|
|
109
|
+
sort_keys=True,
|
|
110
|
+
).encode()
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
package = read_rlpack(bundle)
|
|
114
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
args.output.write_bytes(bundle)
|
|
116
|
+
print(args.output)
|
|
117
|
+
print(package.manifest_digest)
|
|
118
|
+
print(package.archive_digest)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"output": {
|
|
3
|
+
"reply": "We apologize for the delay with your order. Our team is currently investigating the issue and working to resolve it as quickly as possible. You can expect an update on the timeline for your order shortly. Thank you for your patience."
|
|
4
|
+
},
|
|
5
|
+
"context": {}
|
|
6
|
+
}
|
|
@@ -23,12 +23,16 @@ from .rlpack import RlpackError, read_rlpack
|
|
|
23
23
|
|
|
24
24
|
def parser() -> argparse.ArgumentParser:
|
|
25
25
|
root = argparse.ArgumentParser(prog="rubricloop")
|
|
26
|
-
root.add_argument("--server", default=DEFAULT_SERVER)
|
|
26
|
+
root.add_argument("--server", default=DEFAULT_SERVER, help=argparse.SUPPRESS)
|
|
27
27
|
commands = root.add_subparsers(dest="command", required=True)
|
|
28
28
|
login = commands.add_parser("login")
|
|
29
29
|
login.add_argument("--api-key")
|
|
30
30
|
init = commands.add_parser("init")
|
|
31
31
|
init.add_argument("directory", nargs="?", default=".")
|
|
32
|
+
create = commands.add_parser("create")
|
|
33
|
+
create.add_argument("repository")
|
|
34
|
+
create.add_argument("--description", default="")
|
|
35
|
+
create.add_argument("--visibility", choices=("public", "private"), default="private")
|
|
32
36
|
for command in ("validate", "test"):
|
|
33
37
|
item = commands.add_parser(command)
|
|
34
38
|
item.add_argument("package")
|
|
@@ -89,6 +93,20 @@ def execute(args: argparse.Namespace) -> dict | str:
|
|
|
89
93
|
+ "\n"
|
|
90
94
|
)
|
|
91
95
|
return str(target)
|
|
96
|
+
if args.command == "create":
|
|
97
|
+
namespace, slash, name = args.repository.partition("/")
|
|
98
|
+
if not slash or not namespace or not name or "/" in name:
|
|
99
|
+
raise RegistryClientError("repository must be namespace/name")
|
|
100
|
+
return client.json(
|
|
101
|
+
"POST",
|
|
102
|
+
"/v1/registry/repositories",
|
|
103
|
+
payload={
|
|
104
|
+
"namespace": namespace,
|
|
105
|
+
"name": name,
|
|
106
|
+
"description": args.description,
|
|
107
|
+
"visibility": args.visibility,
|
|
108
|
+
},
|
|
109
|
+
)
|
|
92
110
|
if args.command in {"validate", "test"}:
|
|
93
111
|
package = _package(args.package)
|
|
94
112
|
if args.command == "validate":
|
|
@@ -16,7 +16,7 @@ from .refund import (
|
|
|
16
16
|
)
|
|
17
17
|
from .security import CVE_SCHEMA_VERSION, check_cve_record
|
|
18
18
|
from .sql import sql_safe
|
|
19
|
-
from .support import check_support_agent
|
|
19
|
+
from .support import check_support_agent, check_support_reply
|
|
20
20
|
|
|
21
21
|
__all__ = [
|
|
22
22
|
"CVE_SCHEMA_VERSION",
|
|
@@ -37,6 +37,7 @@ __all__ = [
|
|
|
37
37
|
"check_refund",
|
|
38
38
|
"check_shipment_docs",
|
|
39
39
|
"check_support_agent",
|
|
40
|
+
"check_support_reply",
|
|
40
41
|
"official_manifest",
|
|
41
42
|
"official_packages",
|
|
42
43
|
"refund_policy_pack",
|
|
@@ -32,6 +32,13 @@ SUPPORT_CONTEXT = {
|
|
|
32
32
|
},
|
|
33
33
|
"now": "2026-09-23T09:00:00Z",
|
|
34
34
|
}
|
|
35
|
+
SUPPORT_REPLY_OUTPUT = {
|
|
36
|
+
"reply": (
|
|
37
|
+
"We apologize for the delay with your order. Our team is currently investigating "
|
|
38
|
+
"the issue and working to resolve it as quickly as possible. You can expect an "
|
|
39
|
+
"update on the timeline for your order shortly. Thank you for your patience."
|
|
40
|
+
)
|
|
41
|
+
}
|
|
35
42
|
|
|
36
43
|
COMMERCE_OUTPUT = {
|
|
37
44
|
"quote_id": "Q-100",
|
|
@@ -118,6 +125,18 @@ SQL_OUTPUT = {"query": "SELECT name FROM users WHERE country = 'DE'"}
|
|
|
118
125
|
SQL_CONTEXT = {"scenario": "users-by-country"}
|
|
119
126
|
|
|
120
127
|
PACKS = (
|
|
128
|
+
PackMetadata(
|
|
129
|
+
id="support/reply-v1",
|
|
130
|
+
version="1",
|
|
131
|
+
name="Support reply",
|
|
132
|
+
description="Checks reply length, timeline wording, and unsupported refund promises.",
|
|
133
|
+
category="Support",
|
|
134
|
+
tier="oss",
|
|
135
|
+
status="preview",
|
|
136
|
+
execution="gate_only",
|
|
137
|
+
input_example=SUPPORT_REPLY_OUTPUT,
|
|
138
|
+
context_example={},
|
|
139
|
+
),
|
|
121
140
|
PackMetadata(
|
|
122
141
|
id="support/agent-v1",
|
|
123
142
|
version="1",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -9,6 +10,40 @@ from ..checks.structured import text_contains_all
|
|
|
9
10
|
from .base import PackCheck, PackResult
|
|
10
11
|
from .refund import RefundOrder, RefundPolicy, check_refund
|
|
11
12
|
|
|
13
|
+
_WORD = re.compile(r"\b[\w'-]+\b", re.UNICODE)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_support_reply(output: dict[str, Any], context: dict[str, Any]) -> PackResult:
|
|
17
|
+
"""Check the same reply rules used by the public Quick Start."""
|
|
18
|
+
del context
|
|
19
|
+
checks = PackCheck()
|
|
20
|
+
reply = output.get("reply")
|
|
21
|
+
text = reply if isinstance(reply, str) else ""
|
|
22
|
+
word_count = len(_WORD.findall(text))
|
|
23
|
+
|
|
24
|
+
checks.require(
|
|
25
|
+
40 <= word_count <= 70,
|
|
26
|
+
rule_id="reply.length",
|
|
27
|
+
name="reply length",
|
|
28
|
+
evidence=f"reply contains {word_count} words; expected 40 to 70",
|
|
29
|
+
hint="write a reply containing 40 to 70 words",
|
|
30
|
+
)
|
|
31
|
+
checks.require(
|
|
32
|
+
"timeline" in text.casefold(),
|
|
33
|
+
rule_id="reply.timeline",
|
|
34
|
+
name="timeline wording",
|
|
35
|
+
evidence="reply does not contain the word timeline",
|
|
36
|
+
hint="explain the timeline in the reply",
|
|
37
|
+
)
|
|
38
|
+
checks.require(
|
|
39
|
+
"guaranteed refund" not in text.casefold(),
|
|
40
|
+
rule_id="reply.promise",
|
|
41
|
+
name="refund promise",
|
|
42
|
+
evidence="reply contains the forbidden phrase guaranteed refund",
|
|
43
|
+
hint="remove the unsupported refund promise",
|
|
44
|
+
)
|
|
45
|
+
return checks.result(output, metrics={"word_count": word_count})
|
|
46
|
+
|
|
12
47
|
|
|
13
48
|
def check_support_agent(output: dict[str, Any], context: dict[str, Any]) -> PackResult:
|
|
14
49
|
checks = PackCheck()
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
from rubricloop.cli import execute, parser
|
|
8
8
|
from rubricloop.registry import (
|
|
9
|
+
DEFAULT_SERVER,
|
|
9
10
|
RegistryClient,
|
|
10
11
|
RegistryClientError,
|
|
11
12
|
load_credential,
|
|
@@ -15,8 +16,25 @@ from rubricloop.registry import (
|
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def test_cli_defaults_to_hosted_registry():
|
|
20
|
+
assert DEFAULT_SERVER == "https://api.rubricloop.dev"
|
|
21
|
+
assert parser().parse_args(["login"]).server == DEFAULT_SERVER
|
|
22
|
+
assert "--server" not in parser().format_help()
|
|
23
|
+
|
|
24
|
+
|
|
18
25
|
def test_cli_exposes_complete_lifecycle():
|
|
19
|
-
for command in (
|
|
26
|
+
for command in (
|
|
27
|
+
"login",
|
|
28
|
+
"init",
|
|
29
|
+
"create",
|
|
30
|
+
"validate",
|
|
31
|
+
"test",
|
|
32
|
+
"push",
|
|
33
|
+
"vet",
|
|
34
|
+
"publish",
|
|
35
|
+
"pull",
|
|
36
|
+
"run",
|
|
37
|
+
):
|
|
20
38
|
assert command in parser()._subparsers._group_actions[0].choices
|
|
21
39
|
|
|
22
40
|
|
|
@@ -26,6 +44,42 @@ def test_init_creates_project_without_credentials(tmp_path):
|
|
|
26
44
|
assert json.loads((tmp_path / "rubricloop.json").read_text())["format_version"] == 1
|
|
27
45
|
|
|
28
46
|
|
|
47
|
+
def test_create_calls_repository_endpoint(monkeypatch):
|
|
48
|
+
calls = []
|
|
49
|
+
monkeypatch.setattr(
|
|
50
|
+
RegistryClient,
|
|
51
|
+
"json",
|
|
52
|
+
lambda self, method, path, **kwargs: calls.append((method, path, kwargs)) or {"id": "repo"},
|
|
53
|
+
)
|
|
54
|
+
result = execute(
|
|
55
|
+
parser().parse_args(
|
|
56
|
+
[
|
|
57
|
+
"create",
|
|
58
|
+
"acme/reply-check",
|
|
59
|
+
"--visibility",
|
|
60
|
+
"public",
|
|
61
|
+
"--description",
|
|
62
|
+
"Checks support replies.",
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
assert result == {"id": "repo"}
|
|
67
|
+
assert calls == [
|
|
68
|
+
(
|
|
69
|
+
"POST",
|
|
70
|
+
"/v1/registry/repositories",
|
|
71
|
+
{
|
|
72
|
+
"payload": {
|
|
73
|
+
"namespace": "acme",
|
|
74
|
+
"name": "reply-check",
|
|
75
|
+
"description": "Checks support replies.",
|
|
76
|
+
"visibility": "public",
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
29
83
|
def test_credentials_use_keychain_or_explicit_environment(monkeypatch):
|
|
30
84
|
values = {}
|
|
31
85
|
backend_type = type("Backend", (), {})
|
|
@@ -9,6 +9,7 @@ from rubricloop.packs import (
|
|
|
9
9
|
check_quote_invoice,
|
|
10
10
|
check_shipment_docs,
|
|
11
11
|
check_support_agent,
|
|
12
|
+
check_support_reply,
|
|
12
13
|
)
|
|
13
14
|
from rubricloop.packs.catalog import (
|
|
14
15
|
COMMERCE_CONTEXT,
|
|
@@ -31,6 +32,32 @@ def test_support_golden_set(case):
|
|
|
31
32
|
assert check_support_agent(output, deepcopy(SUPPORT_CONTEXT)).decision == "allow"
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def test_support_reply_matches_quick_start_rules():
|
|
36
|
+
valid = check_support_reply(
|
|
37
|
+
{
|
|
38
|
+
"reply": (
|
|
39
|
+
"We apologize for the delay with your order. Our team is currently "
|
|
40
|
+
"investigating the issue and working to resolve it as quickly as possible. "
|
|
41
|
+
"You can expect an update on the timeline for your order shortly. Thank "
|
|
42
|
+
"you for your patience."
|
|
43
|
+
)
|
|
44
|
+
},
|
|
45
|
+
{},
|
|
46
|
+
)
|
|
47
|
+
invalid = check_support_reply(
|
|
48
|
+
{"reply": "We promise a guaranteed refund and will update you soon."},
|
|
49
|
+
{},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert valid.decision == "allow"
|
|
53
|
+
assert invalid.decision == "deny"
|
|
54
|
+
assert {failure.rule_id for failure in invalid.failures} == {
|
|
55
|
+
"reply.length",
|
|
56
|
+
"reply.timeline",
|
|
57
|
+
"reply.promise",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
34
61
|
@pytest.mark.parametrize("case", range(20))
|
|
35
62
|
def test_commerce_golden_set(case):
|
|
36
63
|
output = deepcopy(COMMERCE_OUTPUT)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|