agentic-dataset-conformance 0.1.0__py3-none-any.whl

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 (32) hide show
  1. agentic_dataset_conformance/__init__.py +68 -0
  2. agentic_dataset_conformance/__main__.py +4 -0
  3. agentic_dataset_conformance/cli.py +169 -0
  4. agentic_dataset_conformance/data/LICENSE +13 -0
  5. agentic_dataset_conformance/data/vectors/ad-001-descriptor-valid.json +28 -0
  6. agentic_dataset_conformance/data/vectors/ad-002-capability-registered.json +122 -0
  7. agentic_dataset_conformance/data/vectors/ad-003-grant-required-for-execution.json +30 -0
  8. agentic_dataset_conformance/data/vectors/ad-004-refusal-has-no-grant.json +75 -0
  9. agentic_dataset_conformance/data/vectors/ad-005-indeterminate-has-no-grant.json +41 -0
  10. agentic_dataset_conformance/data/vectors/ad-006-unknown-capability-denied.json +58 -0
  11. agentic_dataset_conformance/data/vectors/ad-007-authorization-scope-preserved.json +36 -0
  12. agentic_dataset_conformance/data/vectors/ad-008-cache-is-policy-scoped.json +80 -0
  13. agentic_dataset_conformance/data/vectors/ad-009-provenance-complete.json +52 -0
  14. agentic_dataset_conformance/data/vectors/ad-010-refusal-recorded.json +20 -0
  15. agentic_dataset_conformance/data/vectors/ad-011-dataset-revision-recorded.json +30 -0
  16. agentic_dataset_conformance/data/vectors/ad-012-policy-version-recorded.json +27 -0
  17. agentic_dataset_conformance/data/vectors/ad-013-mcp-preserves-scope.json +71 -0
  18. agentic_dataset_conformance/data/vectors/ad-014-a2a-preserves-scope.json +71 -0
  19. agentic_dataset_conformance/data/vectors/ad-015-prohibited-execution-rate-zero.json +476 -0
  20. agentic_dataset_conformance/data/worlds/reference.json +254 -0
  21. agentic_dataset_conformance/interface.py +133 -0
  22. agentic_dataset_conformance/mutations.py +283 -0
  23. agentic_dataset_conformance/runner.py +357 -0
  24. agentic_dataset_conformance/toy.py +350 -0
  25. agentic_dataset_conformance/verbs.md +43 -0
  26. agentic_dataset_conformance-0.1.0.dist-info/METADATA +116 -0
  27. agentic_dataset_conformance-0.1.0.dist-info/RECORD +32 -0
  28. agentic_dataset_conformance-0.1.0.dist-info/WHEEL +5 -0
  29. agentic_dataset_conformance-0.1.0.dist-info/entry_points.txt +2 -0
  30. agentic_dataset_conformance-0.1.0.dist-info/licenses/LICENSES/Apache-2.0.txt +202 -0
  31. agentic_dataset_conformance-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +121 -0
  32. agentic_dataset_conformance-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,68 @@
1
+ """The portable conformance suite for the agentic-dataset contract.
2
+
3
+ Fifteen normative assertions, AD-001 .. AD-015, as language-neutral executable
4
+ vectors plus a runner that evaluates them against any implementation through a
5
+ four-method interface. It imports no implementation, including the reference
6
+ one, and the vectors travel with it.
7
+
8
+ from agentic_dataset_conformance import load_suite, run
9
+ report = run(my_subject)
10
+ print(report.passed, [f.assertion for f in report.failures])
11
+
12
+ The vectors and worlds are the normative artifact and are dedicated to the
13
+ public domain under CC0-1.0; the software around them is Apache-2.0. Copy the
14
+ vectors into a Rust, Go or TypeScript project and write your own runner --
15
+ that is what they are for, and `export_vectors()` exists to make it one call.
16
+
17
+ See `verbs.md` beside this module for the control-verb vocabulary a subject
18
+ must understand, and `interface.py` for what it must expose.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import shutil
24
+ from importlib import resources
25
+ from pathlib import Path
26
+
27
+ from .interface import ConformanceSubject, Observation, Scope
28
+ from .runner import (
29
+ INVARIANTS,
30
+ AssertionResult,
31
+ SubjectReport,
32
+ Vector,
33
+ VectorSuite,
34
+ load_suite,
35
+ run,
36
+ )
37
+ from .toy import ToyImplementation
38
+
39
+ __version__ = "0.1.0"
40
+
41
+ #: The fifteen assertion identifiers, in order.
42
+ ASSERTIONS = tuple(f"AD-{i:03d}" for i in range(1, 16))
43
+
44
+ __all__ = [
45
+ "ASSERTIONS", "AssertionResult", "ConformanceSubject", "INVARIANTS",
46
+ "Observation", "Scope", "SubjectReport", "ToyImplementation", "Vector",
47
+ "VectorSuite", "__version__", "export_vectors", "load_suite", "run",
48
+ "vectors_path",
49
+ ]
50
+
51
+
52
+ def vectors_path():
53
+ """The packaged normative data as a traversable resource."""
54
+ return resources.files(__package__) / "data"
55
+
56
+
57
+ def export_vectors(destination: str | Path) -> Path:
58
+ """Copy the normative worlds and vectors out, for use anywhere.
59
+
60
+ They are CC0-1.0: no attribution required, no conditions. Vendor them into
61
+ another language's repository and write a runner there.
62
+ """
63
+ destination = Path(destination)
64
+ destination.mkdir(parents=True, exist_ok=True)
65
+ with resources.as_file(vectors_path()) as data:
66
+ for sub in ("worlds", "vectors"):
67
+ shutil.copytree(data / sub, destination / sub, dirs_exist_ok=True)
68
+ return destination
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,169 @@
1
+ """Command line for the portable conformance suite.
2
+
3
+ agentic-dataset-conformance run # against the built-in toy
4
+ agentic-dataset-conformance run --subject mod:make # against yours
5
+ agentic-dataset-conformance run --matrix # mutation characterisation
6
+ agentic-dataset-conformance vectors --list
7
+ agentic-dataset-conformance vectors --export ./vectors
8
+
9
+ `--subject` takes `module:attribute`. The attribute may be a subject, a
10
+ callable returning one, or a callable returning several. Nothing about
11
+ resolving it is specific to any implementation, which is the point: a
12
+ conforming implementation in another package is tested by naming it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import importlib
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from . import ASSERTIONS, export_vectors
24
+ from .runner import load_suite, run
25
+
26
+
27
+ def _resolve(spec: str) -> list:
28
+ module_name, _, attribute = spec.partition(":")
29
+ if not attribute:
30
+ raise SystemExit(f"--subject expects module:attribute, got {spec!r}")
31
+ sys.path.insert(0, str(Path.cwd()))
32
+ module = importlib.import_module(module_name)
33
+ target = getattr(module, attribute)
34
+ found = target() if callable(target) else target
35
+ return list(found) if isinstance(found, (list, tuple)) else [found]
36
+
37
+
38
+ def _subjects(specs: list[str]) -> list:
39
+ if not specs:
40
+ from .toy import ToyImplementation
41
+
42
+ return [ToyImplementation()]
43
+ return [s for spec in specs for s in _resolve(spec)]
44
+
45
+
46
+ def _matrix(rows: list[tuple[str, str, bool, list[str]]]) -> str:
47
+ labels = [f"M{i:02d}" for i in range(1, len(rows) + 1)]
48
+ out = [" " + " ".join(labels), " " + " ".join("---" for _ in labels)]
49
+ for assertion in ASSERTIONS:
50
+ cells = []
51
+ for _, target, ok, caught in rows:
52
+ if assertion == target:
53
+ cells.append(" T " if ok else " ! ")
54
+ elif assertion in caught:
55
+ cells.append(" x ")
56
+ else:
57
+ cells.append(" . ")
58
+ detected = sum(1 for c in cells if c.strip() in ("T", "x"))
59
+ out.append(f"{assertion} {' '.join(cells)} {detected}")
60
+ out.append("")
61
+ for label, (name, target, ok, _) in zip(labels, rows):
62
+ out.append(f"{label} {target} {name.removeprefix('mutant:')}"
63
+ + ("" if ok else " ** NOT CAUGHT BY ITS TARGET **"))
64
+ caught_n = sum(1 for _, _, ok, _ in rows if ok)
65
+ covered = {t for _, t, _, _ in rows}
66
+ out += [
67
+ "",
68
+ f"target detection : {caught_n}/{len(rows)} mutants caught by their intended assertion",
69
+ f"cross-detection : {sum(len(c) for _, _, _, c in rows) / len(rows):.1f} "
70
+ "assertions per mutant on average",
71
+ f"coverage : {len(covered)}/15 assertions have a mutant of their own",
72
+ "",
73
+ "T = caught by its target assertion x = caught redundantly",
74
+ ". = not detected ! = target failed to catch it",
75
+ ]
76
+ return "\n".join(out)
77
+
78
+
79
+ def _run(args: argparse.Namespace) -> int:
80
+ suite = load_suite(args.vectors)
81
+ reports = [run(s, suite) for s in _subjects(args.subject)]
82
+ failed = any(not r.passed for r in reports)
83
+
84
+ rows: list[tuple[str, str, bool, list[str]]] = []
85
+ if args.mutants or args.matrix:
86
+ from .mutations import TARGETS
87
+
88
+ for cls, target in TARGETS.items():
89
+ caught = [f.assertion for f in run(cls(), suite).failures]
90
+ rows.append((cls.name, target, target in caught, caught))
91
+ failed = failed or any(not ok for _, _, ok, _ in rows)
92
+
93
+ if args.json:
94
+ print(json.dumps({
95
+ "vectors": len(suite.vectors),
96
+ "subjects": [r.to_dict() for r in reports],
97
+ "mutants": [{"mutant": n, "target": t, "caught": ok, "caught_by": by}
98
+ for n, t, ok, by in rows],
99
+ }, indent=2))
100
+ return 1 if failed else 0
101
+
102
+ width = max((len(r.subject) for r in reports), default=10)
103
+ print(f"{len(suite.vectors)} vectors, {sum(len(v.steps) for v in suite.vectors)} steps, "
104
+ f"{len(reports)} subject(s)\n")
105
+ print(f"{'SUBJECT':<{width}} RESULT ASSERTIONS OBSERVATIONS")
106
+ for report in reports:
107
+ passed = len(report.results) - len(report.failures)
108
+ print(f"{report.subject:<{width}} {'PASS' if report.passed else 'FAIL':<6} "
109
+ f"{passed:>6}/{len(report.results)} {report.observations:>12}")
110
+ for failure in report.failures:
111
+ print(f" {failure.assertion}: {failure.detail}")
112
+ if rows and args.matrix:
113
+ print()
114
+ print(_matrix(rows))
115
+ elif rows:
116
+ print(f"\n{'MUTANT':<42} {'TARGET':<8} CAUGHT BY")
117
+ for name, target, ok, caught in rows:
118
+ print(f"{name:<42} {target:<8} {'' if ok else 'MISSED '}"
119
+ f"{','.join(caught) or 'nothing'}")
120
+ return 1 if failed else 0
121
+
122
+
123
+ def _vectors(args: argparse.Namespace) -> int:
124
+ if args.export:
125
+ where = export_vectors(args.export)
126
+ print(f"normative worlds and vectors written to {where} (CC0-1.0, "
127
+ "no attribution required)")
128
+ return 0
129
+ suite = load_suite(args.vectors)
130
+ print(f"{len(suite.vectors)} vectors, "
131
+ f"{sum(len(v.steps) for v in suite.vectors)} steps\n")
132
+ for vector in suite.vectors:
133
+ print(f"{vector.assertion} {vector.name:<44} {len(vector.steps):>3} steps")
134
+ print(f" rules out: {vector.rules_out}")
135
+ return 0
136
+
137
+
138
+ def main(argv: list[str] | None = None) -> int:
139
+ parser = argparse.ArgumentParser(
140
+ prog="agentic-dataset-conformance",
141
+ description="Run AD-001..AD-015 against any implementation.",
142
+ )
143
+ sub = parser.add_subparsers(dest="command")
144
+
145
+ r = sub.add_parser("run", help="evaluate a subject against the vectors")
146
+ r.add_argument("--subject", action="append", default=[], metavar="MODULE:ATTR",
147
+ help="an implementation to test; repeatable. Defaults to the "
148
+ "built-in toy subject.")
149
+ r.add_argument("--mutants", action="store_true",
150
+ help="also check that broken variants are caught")
151
+ r.add_argument("--matrix", action="store_true",
152
+ help="print the mutation detection matrix (implies --mutants)")
153
+ r.add_argument("--vectors", metavar="DIR", default=None,
154
+ help="use vectors from DIR instead of the packaged ones")
155
+ r.add_argument("--json", action="store_true", help="machine-readable output")
156
+ r.set_defaults(func=_run)
157
+
158
+ v = sub.add_parser("vectors", help="list or export the normative vectors")
159
+ v.add_argument("--export", metavar="DIR", default=None,
160
+ help="copy the CC0 worlds and vectors into DIR")
161
+ v.add_argument("--vectors", metavar="DIR", default=None,
162
+ help="list vectors from DIR instead of the packaged ones")
163
+ v.set_defaults(func=_vectors)
164
+
165
+ args = parser.parse_args(argv)
166
+ if args.command is None:
167
+ parser.print_help()
168
+ return 0
169
+ return args.func(args)
@@ -0,0 +1,13 @@
1
+ The files in this directory -- the normative worlds and conformance vectors --
2
+ are dedicated to the public domain under CC0 1.0 Universal.
3
+
4
+ SPDX-License-Identifier: CC0-1.0
5
+ Full text: ../../../LICENSES/CC0-1.0.txt
6
+ https://creativecommons.org/publicdomain/zero/1.0/
7
+
8
+ No attribution is required and no conditions are attached. They are meant to be
9
+ copied unchanged into implementations in other languages, and every condition
10
+ would be friction against the only thing they exist for.
11
+
12
+ The software in the parent package is Apache-2.0. The distribution as a whole
13
+ is therefore "Apache-2.0 AND CC0-1.0".
@@ -0,0 +1,28 @@
1
+ {
2
+ "assertion": "AD-001",
3
+ "rules_out": "a dataset participating in admission without a well-formed contract",
4
+ "steps": [
5
+ {
6
+ "op": "register_descriptor",
7
+ "descriptor": {
8
+ "dataset": "broken-dataset",
9
+ "version": "1",
10
+ "description": "no revision, no capabilities"
11
+ }
12
+ },
13
+ {
14
+ "op": "request",
15
+ "principal": "process_engineer",
16
+ "text": "search broken",
17
+ "dataset": "broken-dataset",
18
+ "capability": "search",
19
+ "expect": {
20
+ "decision": "REFUSED",
21
+ "reason": "DESCRIPTOR_INVALID",
22
+ "granted": false,
23
+ "executed": false
24
+ }
25
+ }
26
+ ],
27
+ "world": "reference"
28
+ }
@@ -0,0 +1,122 @@
1
+ {
2
+ "assertion": "AD-002",
3
+ "rules_out": "an executable action with no capability metadata behind it",
4
+ "capability_surface_matches_descriptors": true,
5
+ "steps": [
6
+ {
7
+ "op": "register_descriptor",
8
+ "descriptor": {
9
+ "dataset": "purification-batches",
10
+ "version": "2026.08.31",
11
+ "revision": "s3-etag-4c1f9a",
12
+ "schema_version": "3",
13
+ "description": "Process and analytical data describing purification batches: recovery, yield, step conditions and in-process controls for downstream chromatography.",
14
+ "schemas": [
15
+ "batch_record",
16
+ "in_process_control",
17
+ "step_condition"
18
+ ],
19
+ "capabilities": [
20
+ {
21
+ "name": "search",
22
+ "description": "find batches by free text over batch records",
23
+ "effect": "read",
24
+ "sensitivity": "internal",
25
+ "policy": null,
26
+ "arguments": [
27
+ "query"
28
+ ]
29
+ },
30
+ {
31
+ "name": "compare_batches",
32
+ "description": "compare recovery and step conditions across two batches",
33
+ "effect": "read",
34
+ "sensitivity": "internal",
35
+ "policy": "BPD-DATA-014",
36
+ "arguments": [
37
+ "batch_a",
38
+ "batch_b"
39
+ ]
40
+ },
41
+ {
42
+ "name": "calculate_yield",
43
+ "description": "compute step yield and cumulative recovery for a batch",
44
+ "effect": "compute",
45
+ "sensitivity": "internal",
46
+ "policy": null,
47
+ "arguments": [
48
+ "batch_id"
49
+ ]
50
+ },
51
+ {
52
+ "name": "detect_outliers",
53
+ "description": "flag anomalous batches against the historical distribution",
54
+ "effect": "compute",
55
+ "sensitivity": "confidential",
56
+ "policy": "BPD-DATA-021",
57
+ "arguments": [
58
+ "metric"
59
+ ]
60
+ },
61
+ {
62
+ "name": "phantom_export",
63
+ "effect": "read",
64
+ "sensitivity": "internal",
65
+ "description": "advertised with nothing behind it",
66
+ "policy": null,
67
+ "arguments": []
68
+ }
69
+ ],
70
+ "prohibited": [
71
+ "delete_source",
72
+ "overwrite_batch_record",
73
+ "bypass_governance",
74
+ "expose_restricted_identifiers"
75
+ ],
76
+ "policies": [
77
+ "BPD-DATA-014",
78
+ "BPD-DATA-021"
79
+ ],
80
+ "provenance": {
81
+ "system": "volume",
82
+ "source": "s3"
83
+ },
84
+ "freshness": {
85
+ "maximum_age_s": 86400
86
+ },
87
+ "age_s": 3600,
88
+ "quality": {
89
+ "required_fields": [
90
+ "batch_ids",
91
+ "recovery"
92
+ ]
93
+ },
94
+ "retention": {
95
+ "observations": 1000
96
+ },
97
+ "endpoints": {
98
+ "mcp": "stdio://purification"
99
+ }
100
+ }
101
+ },
102
+ {
103
+ "op": "grant",
104
+ "principal": "process_engineer",
105
+ "dataset": "purification-batches",
106
+ "capability": "phantom_export"
107
+ },
108
+ {
109
+ "op": "request",
110
+ "principal": "process_engineer",
111
+ "text": "export everything",
112
+ "dataset": "purification-batches",
113
+ "capability": "phantom_export",
114
+ "expect": {
115
+ "executed": false,
116
+ "result_present": false,
117
+ "error_contains": "not a registered capability"
118
+ }
119
+ }
120
+ ],
121
+ "world": "reference"
122
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "assertion": "AD-003",
3
+ "rules_out": "execution reachable without an authorization artifact",
4
+ "note": "the general form is the cross-vector invariant executed => granted",
5
+ "steps": [
6
+ {
7
+ "op": "request",
8
+ "principal": "process_engineer",
9
+ "text": "Compare the recovery of batches B001 and B002",
10
+ "expect": {
11
+ "decision": "GRANTED",
12
+ "granted": true,
13
+ "executed": true
14
+ }
15
+ },
16
+ {
17
+ "op": "request",
18
+ "principal": "process_engineer",
19
+ "text": "Calculate the yield for batch B003",
20
+ "grant_ttl_s": -1,
21
+ "expect": {
22
+ "decision": "GRANTED",
23
+ "executed": false,
24
+ "result_present": false,
25
+ "error_contains": "expired"
26
+ }
27
+ }
28
+ ],
29
+ "world": "reference"
30
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "assertion": "AD-004",
3
+ "rules_out": "a refusal that still mints authority",
4
+ "steps": [
5
+ {
6
+ "op": "request",
7
+ "principal": "process_engineer",
8
+ "text": "delete the source",
9
+ "dataset": "purification-batches",
10
+ "capability": "delete_source",
11
+ "prohibited": true,
12
+ "expect": {
13
+ "decision": "REFUSED",
14
+ "reason": "PROHIBITED_OPERATION",
15
+ "policy_id": "AD-POL-004",
16
+ "granted": false,
17
+ "executed": false
18
+ }
19
+ },
20
+ {
21
+ "op": "request",
22
+ "principal": "external_auditor",
23
+ "text": "Compare the recovery of batches B001 and B002",
24
+ "dataset": "purification-batches",
25
+ "capability": "compare_batches",
26
+ "expect": {
27
+ "decision": "REFUSED",
28
+ "reason": "INSUFFICIENT_PRIVILEGE",
29
+ "granted": false,
30
+ "executed": false
31
+ }
32
+ },
33
+ {
34
+ "op": "request",
35
+ "principal": "analyst",
36
+ "text": "detect outliers in recovery",
37
+ "dataset": "purification-batches",
38
+ "capability": "detect_outliers",
39
+ "expect": {
40
+ "decision": "REFUSED",
41
+ "granted": false,
42
+ "executed": false
43
+ }
44
+ },
45
+ {
46
+ "op": "request",
47
+ "principal": "process_engineer",
48
+ "text": "Compare the recovery of batches B001 and B002",
49
+ "dataset": "purification-batches",
50
+ "capability": "compare_batches",
51
+ "expected_schema_version": "99",
52
+ "expect": {
53
+ "decision": "REFUSED",
54
+ "reason": "SCHEMA_VERSION_MISMATCH",
55
+ "granted": false,
56
+ "executed": false
57
+ }
58
+ },
59
+ {
60
+ "op": "request",
61
+ "principal": "process_engineer",
62
+ "text": "search chromatography runs",
63
+ "dataset": "chromatography-results",
64
+ "capability": "search",
65
+ "freshness": 60,
66
+ "expect": {
67
+ "decision": "REFUSED",
68
+ "reason": "FRESHNESS_UNSATISFIABLE",
69
+ "granted": false,
70
+ "executed": false
71
+ }
72
+ }
73
+ ],
74
+ "world": "reference"
75
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "assertion": "AD-005",
3
+ "rules_out": "unknown authority becoming permission",
4
+ "steps": [
5
+ {
6
+ "op": "request",
7
+ "principal": "process_engineer",
8
+ "text": "Compare the recovery of batches B001 and B002",
9
+ "evaluator": {
10
+ "reachable": false,
11
+ "latency_s": 0.0
12
+ },
13
+ "expect": {
14
+ "decision": "INDETERMINATE",
15
+ "reason": "EVALUATOR_UNAVAILABLE",
16
+ "policy_id": null,
17
+ "rationale_present": true,
18
+ "granted": false,
19
+ "executed": false
20
+ }
21
+ },
22
+ {
23
+ "op": "request",
24
+ "principal": "process_engineer",
25
+ "text": "Compare the recovery of batches B001 and B002",
26
+ "evaluator": {
27
+ "reachable": true,
28
+ "latency_s": 5.0
29
+ },
30
+ "expect": {
31
+ "decision": "INDETERMINATE",
32
+ "reason": "EVALUATOR_TIMEOUT",
33
+ "policy_id": null,
34
+ "rationale_present": true,
35
+ "granted": false,
36
+ "executed": false
37
+ }
38
+ }
39
+ ],
40
+ "world": "reference"
41
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "assertion": "AD-006",
3
+ "rules_out": "default-allow on an unregistered tool",
4
+ "steps": [
5
+ {
6
+ "op": "request",
7
+ "principal": "process_engineer",
8
+ "text": "do the thing",
9
+ "dataset": "purification-batches",
10
+ "capability": "query_database",
11
+ "expect": {
12
+ "decision": "REFUSED",
13
+ "reason": "UNREGISTERED_CAPABILITY",
14
+ "granted": false,
15
+ "executed": false
16
+ }
17
+ },
18
+ {
19
+ "op": "request",
20
+ "principal": "process_engineer",
21
+ "text": "do the thing",
22
+ "dataset": "purification-batches",
23
+ "capability": "exfiltrate",
24
+ "expect": {
25
+ "decision": "REFUSED",
26
+ "reason": "UNREGISTERED_CAPABILITY",
27
+ "granted": false,
28
+ "executed": false
29
+ }
30
+ },
31
+ {
32
+ "op": "request",
33
+ "principal": "process_engineer",
34
+ "text": "do the thing",
35
+ "dataset": "purification-batches",
36
+ "capability": "search_all",
37
+ "expect": {
38
+ "decision": "REFUSED",
39
+ "reason": "UNREGISTERED_CAPABILITY",
40
+ "granted": false,
41
+ "executed": false
42
+ }
43
+ },
44
+ {
45
+ "op": "request",
46
+ "principal": "process_engineer",
47
+ "text": "do the thing",
48
+ "dataset": "purification-batches",
49
+ "expect": {
50
+ "decision": "REFUSED",
51
+ "reason": "UNREGISTERED_CAPABILITY",
52
+ "granted": false,
53
+ "executed": false
54
+ }
55
+ }
56
+ ],
57
+ "world": "reference"
58
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "assertion": "AD-007",
3
+ "rules_out": "scope widening between admission and execution",
4
+ "note": "the general form is the cross-vector invariant grant_scope covers executed_scope",
5
+ "steps": [
6
+ {
7
+ "op": "request",
8
+ "principal": "process_engineer",
9
+ "text": "Compare the recovery of batches B001 and B002",
10
+ "expect": {
11
+ "decision": "GRANTED",
12
+ "granted": true
13
+ }
14
+ },
15
+ {
16
+ "op": "delegate",
17
+ "channel": "a2a",
18
+ "dataset": "purification-batches",
19
+ "capability": "compare_batches",
20
+ "scope": {
21
+ "principal_class": "process-engineer",
22
+ "dataset": "purification-batches",
23
+ "capabilities": [
24
+ "compare_batches",
25
+ "detect_outliers"
26
+ ],
27
+ "max_sensitivity": "restricted"
28
+ },
29
+ "expect": {
30
+ "executed": false,
31
+ "error_contains": "widen"
32
+ }
33
+ }
34
+ ],
35
+ "world": "reference"
36
+ }