proofhouse 0.4.2__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 (102) hide show
  1. proofhouse/__init__.py +9 -0
  2. proofhouse/cli.py +140 -0
  3. proofhouse/compiler/__init__.py +12 -0
  4. proofhouse/compiler/adapters/__init__.py +56 -0
  5. proofhouse/compiler/adapters/anthropic.py +305 -0
  6. proofhouse/compiler/adapters/anthropic_schema_subset.py +102 -0
  7. proofhouse/compiler/adapters/base.py +67 -0
  8. proofhouse/compiler/adapters/fake.py +107 -0
  9. proofhouse/compiler/adapters/gemini.py +344 -0
  10. proofhouse/compiler/adapters/gemini_schema_subset.py +143 -0
  11. proofhouse/compiler/adapters/openai.py +219 -0
  12. proofhouse/compiler/adapters/openai_schema_subset.py +85 -0
  13. proofhouse/compiler/api.py +384 -0
  14. proofhouse/compiler/benchmark.py +367 -0
  15. proofhouse/compiler/canonical.py +97 -0
  16. proofhouse/compiler/capability.py +55 -0
  17. proofhouse/compiler/cli_compiler.py +887 -0
  18. proofhouse/compiler/closed_loop.py +642 -0
  19. proofhouse/compiler/codegen/__init__.py +0 -0
  20. proofhouse/compiler/codegen/typescript.py +126 -0
  21. proofhouse/compiler/contracts.py +293 -0
  22. proofhouse/compiler/diagnostics.py +159 -0
  23. proofhouse/compiler/eval_aggregate.py +28 -0
  24. proofhouse/compiler/eval_dataset.py +80 -0
  25. proofhouse/compiler/eval_product.py +184 -0
  26. proofhouse/compiler/eval_rubric.py +61 -0
  27. proofhouse/compiler/evaluation.py +79 -0
  28. proofhouse/compiler/evidence.py +89 -0
  29. proofhouse/compiler/execution.py +441 -0
  30. proofhouse/compiler/hosted_openapi.py +158 -0
  31. proofhouse/compiler/hosted_slice.py +270 -0
  32. proofhouse/compiler/immutability.py +71 -0
  33. proofhouse/compiler/intake.py +213 -0
  34. proofhouse/compiler/ir.py +104 -0
  35. proofhouse/compiler/missionrig.py +148 -0
  36. proofhouse/compiler/model_suggest.py +89 -0
  37. proofhouse/compiler/orchestration/__init__.py +1 -0
  38. proofhouse/compiler/orchestration/assay.py +30 -0
  39. proofhouse/compiler/orchestration/claim_ledger.py +21 -0
  40. proofhouse/compiler/orchestration/learning.py +111 -0
  41. proofhouse/compiler/orchestration/overseer.py +9 -0
  42. proofhouse/compiler/orchestration/proof.py +50 -0
  43. proofhouse/compiler/orchestration/route.py +120 -0
  44. proofhouse/compiler/orchestration/shipwalk_consume.py +26 -0
  45. proofhouse/compiler/orchestration/stack_profiles.py +77 -0
  46. proofhouse/compiler/passes/__init__.py +20 -0
  47. proofhouse/compiler/passes/adapter_lowering.py +159 -0
  48. proofhouse/compiler/passes/base.py +46 -0
  49. proofhouse/compiler/passes/capability_resolution.py +103 -0
  50. proofhouse/compiler/passes/normalization.py +32 -0
  51. proofhouse/compiler/passes/optimization.py +17 -0
  52. proofhouse/compiler/passes/safety.py +69 -0
  53. proofhouse/compiler/passes/validation.py +68 -0
  54. proofhouse/compiler/paths.py +55 -0
  55. proofhouse/compiler/pipeline.py +76 -0
  56. proofhouse/compiler/plain_language.py +134 -0
  57. proofhouse/compiler/repair.py +47 -0
  58. proofhouse/compiler/requirements_contract.py +774 -0
  59. proofhouse/compiler/requirements_ir_bridge.py +222 -0
  60. proofhouse/compiler/requirements_plain_produce.py +193 -0
  61. proofhouse/compiler/requirements_produce.py +181 -0
  62. proofhouse/compiler/resource_bounds.py +8 -0
  63. proofhouse/compiler/runtime_context.py +235 -0
  64. proofhouse/compiler/schemas/benchmark-manifest.schema.json +108 -0
  65. proofhouse/compiler/schemas/diagnostic_code_registry.json +30 -0
  66. proofhouse/compiler/schemas/diagnostic_contract.schema.json +47 -0
  67. proofhouse/compiler/schemas/proofhouse_ir_v0_1.schema.json +217 -0
  68. proofhouse/compiler/schemas/requirements_diagnostic_registry.json +35 -0
  69. proofhouse/compiler/sink.py +55 -0
  70. proofhouse/loadouts.py +110 -0
  71. proofhouse/optimize/__init__.py +5 -0
  72. proofhouse/optimize/case.py +444 -0
  73. proofhouse/optimize/checks.py +416 -0
  74. proofhouse/optimize/cli.py +759 -0
  75. proofhouse/optimize/cli_workflow.py +342 -0
  76. proofhouse/optimize/data/model_registry.json +253 -0
  77. proofhouse/optimize/data/proofhouse-framework.json +402 -0
  78. proofhouse/optimize/data/proofhouse.skill +0 -0
  79. proofhouse/optimize/data/reference-advisory/advisory.txt +10 -0
  80. proofhouse/optimize/data/reference-advisory/answers.json +5 -0
  81. proofhouse/optimize/data/reference-advisory/objective.txt +1 -0
  82. proofhouse/optimize/data/reference-advisory/output-v1.txt +1 -0
  83. proofhouse/optimize/data/reference-advisory/output-v2.txt +1 -0
  84. proofhouse/optimize/data/reference-advisory/prompt-v1.txt +1 -0
  85. proofhouse/optimize/data/reference-advisory/prompt-v2.txt +1 -0
  86. proofhouse/optimize/install_skill.py +360 -0
  87. proofhouse/optimize/model_notes.py +296 -0
  88. proofhouse/optimize/packets.py +182 -0
  89. proofhouse/optimize/reference_workflow.py +218 -0
  90. proofhouse/optimize/registry.py +114 -0
  91. proofhouse/optimize/workflow.py +507 -0
  92. proofhouse/runner.py +66 -0
  93. proofhouse/scan.py +351 -0
  94. proofhouse/schemas.py +51 -0
  95. proofhouse/scoring.py +30 -0
  96. proofhouse/templates.py +168 -0
  97. proofhouse-0.4.2.dist-info/METADATA +260 -0
  98. proofhouse-0.4.2.dist-info/RECORD +102 -0
  99. proofhouse-0.4.2.dist-info/WHEEL +5 -0
  100. proofhouse-0.4.2.dist-info/entry_points.txt +3 -0
  101. proofhouse-0.4.2.dist-info/licenses/LICENSE +21 -0
  102. proofhouse-0.4.2.dist-info/top_level.txt +1 -0
proofhouse/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Proofhouse prompt-operations framework."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ # One source of truth: the installed distribution's version (pyproject.toml).
7
+ __version__ = version("proofhouse")
8
+ except PackageNotFoundError: # running from a source tree that was never installed
9
+ __version__ = "0+unknown"
proofhouse/cli.py ADDED
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .loadouts import compile_loadout, load_legendary_loadouts
9
+ from .runner import build_markdown_report, validate_dataset
10
+ from .scan import ScanError, emit as emit_scan, render as render_scan, scan as scan_project
11
+ from .templates import PromptArchitectInputs, export_prompt_architect
12
+
13
+
14
+ def cmd_validate(args: argparse.Namespace) -> int:
15
+ issues = validate_dataset(args.dataset)
16
+ if issues:
17
+ print(f"Dataset validation failed: {args.dataset}")
18
+ for issue in issues:
19
+ print(f"line {issue.line}: {issue.message}")
20
+ return 1
21
+ print(f"Dataset validation passed: {args.dataset}")
22
+ return 0
23
+
24
+
25
+ def cmd_report(args: argparse.Namespace) -> int:
26
+ report = build_markdown_report(args.dataset)
27
+ out = Path(args.out)
28
+ out.parent.mkdir(parents=True, exist_ok=True)
29
+ out.write_text(report, encoding="utf-8")
30
+ print(f"Report written: {out}")
31
+ return 0
32
+
33
+
34
+ def cmd_loadouts(args: argparse.Namespace) -> int:
35
+ for loadout in load_legendary_loadouts():
36
+ print(f"{loadout.id}\t{loadout.name}\t{loadout.role}")
37
+ return 0
38
+
39
+
40
+ def cmd_compile_loadout(args: argparse.Namespace) -> int:
41
+ compiled = compile_loadout(args.id)
42
+ if args.out:
43
+ out = Path(args.out)
44
+ out.parent.mkdir(parents=True, exist_ok=True)
45
+ out.write_text(compiled, encoding="utf-8")
46
+ print(f"Loadout compiled: {out}")
47
+ else:
48
+ print(compiled, end="")
49
+ return 0
50
+
51
+
52
+ def cmd_scan(args: argparse.Namespace) -> int:
53
+ try:
54
+ result = scan_project(args.path)
55
+ except ScanError as exc:
56
+ print(f"scan: {exc}", file=sys.stderr)
57
+ return 2
58
+ emit_scan(json.dumps(result, indent=2) if args.json else render_scan(result))
59
+ return 0
60
+
61
+
62
+ def cmd_generate(args: argparse.Namespace) -> int:
63
+ if args.template != "prompt-architect":
64
+ raise ValueError(f"Unsupported template: {args.template}")
65
+
66
+ exported = export_prompt_architect(
67
+ PromptArchitectInputs(
68
+ project_name=args.project_name,
69
+ project_description=args.project_description,
70
+ platforms=args.platform or [],
71
+ stack=args.stack or "",
72
+ scale=args.scale or "",
73
+ open_decisions=args.open_decision or [],
74
+ ),
75
+ args.out_dir,
76
+ version=args.version,
77
+ )
78
+ print(f"Prompt architect templates rendered with version {exported.version}:")
79
+ print(f"System: {exported.system_path}")
80
+ print(f"Compact: {exported.compact_path}")
81
+ return 0
82
+
83
+
84
+ def build_parser() -> argparse.ArgumentParser:
85
+ parser = argparse.ArgumentParser(prog="proofhouse", description="Proofhouse eval harness")
86
+ subparsers = parser.add_subparsers(dest="command", required=True)
87
+
88
+ validate = subparsers.add_parser("validate", help="Validate a JSONL eval dataset")
89
+ validate.add_argument("--dataset", required=True, help="Path to JSONL dataset")
90
+ validate.set_defaults(func=cmd_validate)
91
+
92
+ report = subparsers.add_parser("report", help="Create a markdown report skeleton")
93
+ report.add_argument("--dataset", required=True, help="Path to JSONL dataset")
94
+ report.add_argument("--out", required=True, help="Output markdown path")
95
+ report.set_defaults(func=cmd_report)
96
+
97
+ loadouts = subparsers.add_parser("loadouts", help="List legendary Proofhouse loadouts")
98
+ loadouts.set_defaults(func=cmd_loadouts)
99
+
100
+ compile_parser = subparsers.add_parser("compile-loadout", help="Compile a legendary loadout")
101
+ compile_parser.add_argument("--id", required=True, help="Loadout id to compile")
102
+ compile_parser.add_argument("--out", help="Optional output markdown path")
103
+ compile_parser.set_defaults(func=cmd_compile_loadout)
104
+
105
+ scan_cmd = subparsers.add_parser("scan", help="Read-only scan of a project folder, so fewer questions are needed")
106
+ scan_cmd.add_argument("path", nargs="?", default=".", help="Project folder (default: current folder)")
107
+ scan_cmd.add_argument("--json", action="store_true", help="Machine-readable output")
108
+ scan_cmd.set_defaults(func=cmd_scan)
109
+
110
+ generate = subparsers.add_parser("generate", help="Render versioned prompt templates")
111
+ generate.add_argument("--template", default="prompt-architect", choices=["prompt-architect"])
112
+ generate.add_argument("--version", default=None, help="Template version, defaults to manifest default")
113
+ generate.add_argument("--project-name", required=True, help="Project name to inject")
114
+ generate.add_argument("--project-description", required=True, help="Project description to inject")
115
+ generate.add_argument(
116
+ "--platform",
117
+ action="append",
118
+ help="Target platform; repeat for multiple platforms",
119
+ )
120
+ generate.add_argument("--stack", help="Preferred or existing stack")
121
+ generate.add_argument("--scale", choices=["S", "M", "L", "XL"], help="Project scale")
122
+ generate.add_argument(
123
+ "--open-decision",
124
+ action="append",
125
+ help="Open decision to carry into the prompt; repeat for multiple decisions",
126
+ )
127
+ generate.add_argument("--out-dir", required=True, help="Directory for rendered prompt files")
128
+ generate.set_defaults(func=cmd_generate)
129
+
130
+ return parser
131
+
132
+
133
+ def main() -> int:
134
+ parser = build_parser()
135
+ args = parser.parse_args()
136
+ return args.func(args)
137
+
138
+
139
+ if __name__ == "__main__":
140
+ raise SystemExit(main())
@@ -0,0 +1,12 @@
1
+ """Proofhouse Compiler Core v0.1 — deterministic, offline IR compiler.
2
+
3
+ Public library surface lives in `proofhouse.compiler.api`. Default
4
+ compile/validate/closed-loop paths contain no live provider calls, network
5
+ access, or credential handling. Opt-in live OpenAI execution lives in
6
+ `execution.py` (fail-closed, DEFERRED-to-opt-in, not CERTIFIED).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ COMPILER_ID = "proofhouse-compiler-core"
11
+ COMPILER_VERSION = "0.1.0"
12
+ IR_CONTRACT_VERSION = "0.1.0"
@@ -0,0 +1,56 @@
1
+ """Adapter registry.
2
+
3
+ v0.1 registers the deterministic fake adapter, the OpenAI adapter (second
4
+ conformance target, MISSION-003), the Anthropic adapter (third conformance
5
+ target, MISSION-004) and, as of MISSION-005, the Gemini adapter (fourth and
6
+ final conformance target per OAR-001-02's ratified order). All four adapter
7
+ ids are now registered; no live-provider id remains reserved-but-unimplemented.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from ..diagnostics import DiagnosticFactory
12
+ from .anthropic import ADAPTER_ID as ANTHROPIC_ADAPTER_ID
13
+ from .anthropic import AnthropicAdapter
14
+ from .base import Adapter, AdapterNotFoundError
15
+ from .fake import ADAPTER_ID as FAKE_ADAPTER_ID
16
+ from .fake import FakeAdapter
17
+ from .gemini import ADAPTER_ID as GEMINI_ADAPTER_ID
18
+ from .gemini import GeminiAdapter
19
+ from .openai import ADAPTER_ID as OPENAI_ADAPTER_ID
20
+ from .openai import OpenAIAdapter
21
+
22
+ RESERVED_LIVE_ADAPTER_IDS = frozenset()
23
+ KNOWN_ADAPTER_IDS = frozenset({FAKE_ADAPTER_ID, OPENAI_ADAPTER_ID, ANTHROPIC_ADAPTER_ID, GEMINI_ADAPTER_ID})
24
+
25
+
26
+ def list_registered_adapter_ids() -> tuple[str, ...]:
27
+ """Adapters actually implemented in v0.1 (fake, openai, anthropic, gemini)."""
28
+ return (FAKE_ADAPTER_ID, OPENAI_ADAPTER_ID, ANTHROPIC_ADAPTER_ID, GEMINI_ADAPTER_ID)
29
+
30
+
31
+ def get_adapter(adapter_id: str, diagnostics: DiagnosticFactory, source_document: str = "<input>") -> Adapter:
32
+ if adapter_id == FAKE_ADAPTER_ID:
33
+ return FakeAdapter(diagnostics, source_document)
34
+ if adapter_id == OPENAI_ADAPTER_ID:
35
+ return OpenAIAdapter(diagnostics, source_document)
36
+ if adapter_id == ANTHROPIC_ADAPTER_ID:
37
+ return AnthropicAdapter(diagnostics, source_document)
38
+ if adapter_id == GEMINI_ADAPTER_ID:
39
+ return GeminiAdapter(diagnostics, source_document)
40
+ if adapter_id in RESERVED_LIVE_ADAPTER_IDS:
41
+ raise AdapterNotFoundError(
42
+ f"adapter {adapter_id!r} is a reserved live-provider id not implemented in "
43
+ f"Compiler Core v0.1 (out of scope: live execution); it is never silently "
44
+ f"substituted with another adapter"
45
+ )
46
+ raise AdapterNotFoundError(f"unknown adapter id {adapter_id!r}")
47
+
48
+
49
+ __all__ = [
50
+ "Adapter",
51
+ "AdapterNotFoundError",
52
+ "KNOWN_ADAPTER_IDS",
53
+ "RESERVED_LIVE_ADAPTER_IDS",
54
+ "get_adapter",
55
+ "list_registered_adapter_ids",
56
+ ]
@@ -0,0 +1,305 @@
1
+ """Anthropic adapter -- the third conformance target (OAR-001-02).
2
+
3
+ Produces a deterministic, offline-computable Anthropic-shaped request payload
4
+ from validated IR. Per PROVIDER_ADAPTER_CONTRACT.md, `lower()` never calls
5
+ the Anthropic API, never handles credentials, and never touches the network
6
+ -- live execution is a separate, future interface and permission boundary.
7
+
8
+ Structurally this class parallels `openai.py` (constructed the same way,
9
+ same fail-fast-on-required-gap `lower()` shape, same
10
+ `canonicalize`/`canonical_sha256` artifact primitives, same deliberately
11
+ duplicated `check_capabilities()` loop -- see MISSION_003_REPORT.md's stated
12
+ reasoning for not sharing that loop across adapters). It diverges from
13
+ `openai.py` in three places Anthropic's actual model requires it to, per
14
+ MISSION-004 scope items 3-4:
15
+
16
+ 1. Client-executed vs. server-executed tools are contractually distinct for
17
+ Anthropic (unlike OpenAI's single `tools.function_calling@1` surface), so
18
+ this adapter tracks a separate `tools.server_executed@1` capability and
19
+ never collapses both kinds into one generic `tools` artifact field --
20
+ `lower()` always emits distinct `client_tools` and `server_tools` keys.
21
+ 2. Anthropic's extended-thinking model requires signature-verified
22
+ thinking-block preservation across tool-use turns -- structurally
23
+ different from OpenAI's scalar `reasoning.effort_control@1` -- so this
24
+ adapter uses its own `reasoning.extended_thinking@1` capability id and
25
+ always emits an explicit `thinking` object in the artifact (never
26
+ omitted), even when the frozen IR has no field to source a concrete
27
+ `budget_tokens` value from.
28
+ 3. The strict schema subset is Anthropic-sourced (`anthropic_schema_subset`),
29
+ not reused from `openai_schema_subset`, matching the intentional
30
+ per-adapter duplication precedent from MISSION-003.
31
+
32
+ Capability manifest and the strict-schema-subset limits it references are
33
+ grounded in current Anthropic documentation (Structured Outputs, Strict Tool
34
+ Use, Tool Use overview/server-tools, and Extended Thinking guides, confirmed
35
+ 2026-07-22 via web search cross-checked across multiple independent
36
+ sources -- see MISSION_004_REPORT.md for the full corroboration record).
37
+ """
38
+ from __future__ import annotations
39
+
40
+ from ..canonical import canonical_sha256, canonicalize
41
+ from ..capability import CapabilityManifest
42
+ from ..contracts import Artifact, CapabilityDecision, Diagnostic
43
+ from ..diagnostics import DiagnosticFactory
44
+ from .anthropic_schema_subset import check_strict_subset
45
+ from .base import AdapterDescriptor, LoweringResult
46
+
47
+ ADAPTER_ID = "anthropic"
48
+ ADAPTER_VERSION = "0.1.0"
49
+ PROVIDER_ID = "anthropic"
50
+ CONFORMANCE_SUITE_VERSION = "0.1.0"
51
+
52
+ STRUCTURED_JSON_CAPABILITY = "output.structured_json@1"
53
+ CLIENT_TOOL_CAPABILITY = "tools.function_calling@1"
54
+ SERVER_TOOL_CAPABILITY = "tools.server_executed@1"
55
+ THINKING_CAPABILITY = "reasoning.extended_thinking@1"
56
+
57
+ # tools.server_executed@1 is deliberately NOT in supported/conditional: see
58
+ # CAPABILITY_LIMITS[SERVER_TOOL_CAPABILITY] for why this is a genuine IR
59
+ # representational gap, not a misreading of Anthropic's actual capability.
60
+ SUPPORTED_CAPABILITIES = frozenset({STRUCTURED_JSON_CAPABILITY, CLIENT_TOOL_CAPABILITY})
61
+ CONDITIONAL_CAPABILITIES = frozenset({THINKING_CAPABILITY})
62
+
63
+ _STRICT_SCHEMA_LIMITS = {
64
+ "additional_properties_must_be_false": True,
65
+ "all_properties_must_be_required": True,
66
+ "supported_types": ["string", "number", "integer", "boolean", "array", "object", "null"],
67
+ "documented_but_unenforced_keywords": [
68
+ "minLength",
69
+ "maxLength",
70
+ "pattern",
71
+ "minItems",
72
+ "maxItems",
73
+ "minimum",
74
+ "maximum",
75
+ "multipleOf",
76
+ ],
77
+ "recursive_schemas_supported": False,
78
+ }
79
+
80
+ CAPABILITY_LIMITS = {
81
+ STRUCTURED_JSON_CAPABILITY: {
82
+ **_STRICT_SCHEMA_LIMITS,
83
+ "source": "https://platform.claude.com/docs/en/build-with-claude/structured-outputs",
84
+ },
85
+ CLIENT_TOOL_CAPABILITY: {
86
+ **_STRICT_SCHEMA_LIMITS,
87
+ "applies_when": "strict: true tool definitions (client-executed, caller-defined tools only)",
88
+ "source": "https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use",
89
+ },
90
+ SERVER_TOOL_CAPABILITY: {
91
+ "note": (
92
+ "Anthropic's API genuinely supports server-executed tools (e.g. web_search, "
93
+ "web_fetch, code_execution, tool_search) that run on Anthropic's own infrastructure "
94
+ "with no caller-side handler. Proofhouse's frozen IR v0.1 `tools` array schema "
95
+ "(proofhouse_ir_v0_1.schema.json) only supports the caller-defined custom-tool shape "
96
+ "(id/description/input_schema/side_effecting/approval) -- there is no IR field to "
97
+ "select a specific Anthropic server tool or supply its configuration. This adapter "
98
+ "reports tools.server_executed@1 as unsupported to reflect that IR-representational "
99
+ "gap explicitly, rather than silently downgrading a server-tool request to a client "
100
+ "tool or fabricating a payload for a tool the IR cannot actually describe."
101
+ ),
102
+ "source": "https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools",
103
+ },
104
+ THINKING_CAPABILITY: {
105
+ "note": "extended thinking is model-specific; not available on all Claude models",
106
+ "budget_tokens_minimum": 1024,
107
+ "preservation_required_when_used_with_tools": True,
108
+ "signature_field_required": True,
109
+ "redacted_thinking_blocks_must_be_preserved": True,
110
+ "incompatible_with": ["temperature_override", "top_k_override", "forced_tool_choice"],
111
+ "source": "https://platform.claude.com/docs/en/build-with-claude/extended-thinking",
112
+ },
113
+ }
114
+
115
+
116
+ class AnthropicAdapter:
117
+ adapter_id = ADAPTER_ID
118
+ adapter_version = ADAPTER_VERSION
119
+
120
+ def __init__(self, diagnostics: DiagnosticFactory, source_document: str = "<input>"):
121
+ self._diagnostics = diagnostics
122
+ self._source_document = source_document
123
+
124
+ def capability_manifest(self) -> CapabilityManifest:
125
+ return CapabilityManifest(
126
+ adapter_id=ADAPTER_ID,
127
+ adapter_version=ADAPTER_VERSION,
128
+ manifest_version="0.1.0",
129
+ supported=SUPPORTED_CAPABILITIES,
130
+ conditional=CONDITIONAL_CAPABILITIES,
131
+ limits=CAPABILITY_LIMITS,
132
+ )
133
+
134
+ def describe(self) -> AdapterDescriptor:
135
+ manifest = self.capability_manifest()
136
+ digest = manifest.digest
137
+ return AdapterDescriptor(
138
+ adapter_id=ADAPTER_ID,
139
+ adapter_version=ADAPTER_VERSION,
140
+ provider_id=PROVIDER_ID,
141
+ supported_ir_range="0.1.0",
142
+ capability_manifest_version=manifest.manifest_version,
143
+ capability_manifest_digest=digest,
144
+ artifact_kinds=("anthropic_request_payload",),
145
+ conformance_suite_version=CONFORMANCE_SUITE_VERSION,
146
+ )
147
+
148
+ def check_capabilities(self, validated_ir: dict) -> tuple[CapabilityDecision, ...]:
149
+ provider_requirements = validated_ir.get("provider_requirements") or {
150
+ "required_capabilities": [],
151
+ "optional_capabilities": [],
152
+ }
153
+ manifest = self.capability_manifest()
154
+ decisions: list[CapabilityDecision] = []
155
+ for capability in provider_requirements.get("required_capabilities", []):
156
+ decisions.append(
157
+ CapabilityDecision(capability=capability, requirement="required", resolution=manifest.resolve(capability))
158
+ )
159
+ for capability in provider_requirements.get("optional_capabilities", []):
160
+ decisions.append(
161
+ CapabilityDecision(capability=capability, requirement="optional", resolution=manifest.resolve(capability))
162
+ )
163
+ return tuple(decisions)
164
+
165
+ def lower(self, validated_ir: dict, resolution: tuple[CapabilityDecision, ...]) -> LoweringResult:
166
+ required_gaps = [d for d in resolution if d.requirement == "required" and d.resolution == "unsupported"]
167
+ if required_gaps:
168
+ diags = tuple(
169
+ self._diagnostics.emit(
170
+ code="PRG-CAPABILITY-0001",
171
+ phase="capability_resolution",
172
+ message=f"Required capability {gap.capability!r} unsupported by anthropic adapter.",
173
+ document=self._source_document,
174
+ json_pointer="/provider_requirements/required_capabilities",
175
+ )
176
+ for gap in required_gaps
177
+ )
178
+ return LoweringResult(artifacts=(), diagnostics=diags, status="failure")
179
+
180
+ requested = {d.capability for d in resolution}
181
+ wants_structured_json = STRUCTURED_JSON_CAPABILITY in requested
182
+ wants_client_tools = CLIENT_TOOL_CAPABILITY in requested
183
+ wants_thinking = THINKING_CAPABILITY in requested
184
+
185
+ subset_diagnostics: list[Diagnostic] = []
186
+
187
+ # Anthropic v0.1 has one response schema per request. Composite
188
+ # lowering is an explicit future contract, never an index-zero choice.
189
+ output_format = None
190
+ if wants_structured_json:
191
+ output_contracts = validated_ir.get("output_contracts") or []
192
+ if len(output_contracts) > 1:
193
+ diagnostic = self._diagnostics.emit(
194
+ code="PRG-ADAPTER-0001",
195
+ phase="adapter_lowering",
196
+ message="Anthropic v0.1 lowering rejects multiple output contracts; composite lowering is not authorized.",
197
+ document=self._source_document,
198
+ json_pointer="/output_contracts",
199
+ )
200
+ return LoweringResult(artifacts=(), diagnostics=(diagnostic,), status="failure")
201
+ if output_contracts:
202
+ contract = output_contracts[0]
203
+ schema = contract["schema"]
204
+ violations = check_strict_subset(schema, base_pointer="/output_contracts/0/schema")
205
+ for v in violations:
206
+ subset_diagnostics.append(
207
+ self._diagnostics.emit(
208
+ code="PRG-ADAPTER-0001",
209
+ phase="adapter_lowering",
210
+ message=f"Output schema is not expressible in Anthropic's strict "
211
+ f"structured-output subset: {v.reason}",
212
+ document=self._source_document,
213
+ json_pointer=v.json_pointer,
214
+ )
215
+ )
216
+ if not violations:
217
+ output_format = {"type": "json_schema", "schema": schema}
218
+
219
+ client_tools_payload: list[dict] = []
220
+ if wants_client_tools:
221
+ for idx, tool in enumerate(validated_ir.get("tools") or []):
222
+ schema = tool["input_schema"]
223
+ violations = check_strict_subset(schema, base_pointer=f"/tools/{idx}/input_schema")
224
+ for v in violations:
225
+ subset_diagnostics.append(
226
+ self._diagnostics.emit(
227
+ code="PRG-ADAPTER-0001",
228
+ phase="adapter_lowering",
229
+ message=f"Client tool {tool.get('id')!r} input_schema is not expressible in "
230
+ f"Anthropic's strict tool-use subset: {v.reason}",
231
+ document=self._source_document,
232
+ json_pointer=v.json_pointer,
233
+ )
234
+ )
235
+ if not violations:
236
+ client_tools_payload.append(
237
+ {
238
+ "name": tool["id"],
239
+ "description": tool["description"],
240
+ "input_schema": schema,
241
+ "strict": True,
242
+ }
243
+ )
244
+
245
+ if subset_diagnostics:
246
+ return LoweringResult(artifacts=(), diagnostics=tuple(subset_diagnostics), status="failure")
247
+
248
+ # Client-vs-server tool distinction (scope item 4): two distinct keys,
249
+ # never one flattened generic "tools" field. server_tools is always []
250
+ # here -- the frozen IR v0.1 has no way to express an Anthropic
251
+ # server-executed tool (see CAPABILITY_LIMITS[SERVER_TOOL_CAPABILITY]) --
252
+ # but the key is always explicitly present, and any requested-but-
253
+ # unsupported server-tool capability is recorded in capability_decisions,
254
+ # never silently dropped.
255
+ server_tools_payload: list[dict] = []
256
+
257
+ # Thinking-block/reasoning-preservation state (scope item 4): always an
258
+ # explicit object, never omitted, even when not requested or when the IR
259
+ # has no field to source a concrete budget_tokens value from.
260
+ if wants_thinking:
261
+ thinking_state = {
262
+ "requested": True,
263
+ "capability_resolution": "conditional",
264
+ "budget_tokens": None,
265
+ "budget_tokens_note": (
266
+ "Proofhouse IR v0.1 has no field to source a concrete budget_tokens "
267
+ "value from; recorded as an open IR-schema question, not silently dropped."
268
+ ),
269
+ "preservation": {
270
+ "signature_required": True,
271
+ "must_return_thinking_blocks_unmodified_when_used_with_tools": True,
272
+ "redacted_thinking_blocks_must_be_preserved": True,
273
+ },
274
+ }
275
+ else:
276
+ thinking_state = {"requested": False}
277
+
278
+ payload = {
279
+ "adapter_id": ADAPTER_ID,
280
+ "adapter_version": ADAPTER_VERSION,
281
+ "ir_sha256": canonical_sha256(validated_ir),
282
+ "model_selection": "runtime-configured; not frozen by the compiler artifact",
283
+ "instructions": _build_instructions(validated_ir),
284
+ "client_tools": client_tools_payload,
285
+ "server_tools": server_tools_payload,
286
+ "output_config": {"format": output_format} if output_format is not None else None,
287
+ "thinking": thinking_state,
288
+ "capability_decisions": [d.to_dict() for d in resolution],
289
+ }
290
+ body = canonicalize(payload)
291
+ digest = canonical_sha256(payload)
292
+ artifact = Artifact(
293
+ name="anthropic_request_payload",
294
+ media_type="application/vnd.proofhouse.anthropic.request-payload+json",
295
+ sha256=digest,
296
+ data=body,
297
+ )
298
+ return LoweringResult(artifacts=(artifact,), diagnostics=(), status="success")
299
+
300
+
301
+ def _build_instructions(validated_ir: dict) -> str:
302
+ objective = validated_ir["objective"]
303
+ behavior = validated_ir["behavior"]
304
+ lines = [objective["goal"], *behavior["instructions"], *behavior["constraints"]]
305
+ return "\n".join(lines)
@@ -0,0 +1,102 @@
1
+ """Anthropic's documented strict JSON Schema subset, shared by JSON Outputs
2
+ (`output_config.format`) and strict tool use (`strict: true` tool definitions).
3
+
4
+ Deliberately narrow, following the same discipline as
5
+ `openai_schema_subset.py`: this module encodes only the constraints
6
+ confirmed with confidence against current Anthropic documentation as of
7
+ this mission (2026-07-22), via web search cross-checked across multiple
8
+ independent sources (direct fetches of `platform.claude.com` guide pages
9
+ were not reliably retrievable as body text through the available tools,
10
+ the same limitation MISSION-003 hit for `platform.openai.com`). It is not
11
+ a general JSON Schema subset checker.
12
+
13
+ Confirmed hard constraints (enforced as violations by this module --
14
+ sourced from Anthropic's Structured Outputs and Strict Tool Use guides,
15
+ cross-checked against independent secondary summaries of the same guides):
16
+ - Every object-typed schema node MUST set additionalProperties: false.
17
+ - Every property declared in an object's `properties` MUST also appear
18
+ in that object's `required` array.
19
+ - Supported base types: string, number, integer, boolean, array, object,
20
+ null (standalone or in a type-union list).
21
+
22
+ Confirmed but NOT enforced by this checker (informational only -- see
23
+ ANTHROPIC_CAPABILITY note below and MISSION_004_REPORT.md Technical Debt):
24
+ - Anthropic's strict subset additionally documents support for `enum`,
25
+ `const`, `anyOf`/`allOf`, and `$ref`/`$defs`, which OpenAI's confirmed
26
+ subset (per MISSION-003) did not assert either way. This module does
27
+ not add checks for these keywords' presence/absence; it only flags the
28
+ two hard constraints above.
29
+ - Keywords such as `minLength`, `maxLength`, `pattern`, `minItems`,
30
+ `maxItems`, `minimum`, `maximum`, and `multipleOf` are confirmed, across
31
+ multiple independent sources, to be *accepted but silently unenforced*
32
+ by Anthropic's constrained decoding under strict mode -- this is a
33
+ materially different behavior than "rejected as a violation", so this
34
+ checker deliberately does NOT flag their presence as a subset violation
35
+ (doing so would misrepresent silently-ignored-but-permitted as
36
+ disallowed). Recorded as a known, source-confirmed gap: no diagnostic
37
+ currently warns a schema author that these keywords will be ignored.
38
+ - Recursive schemas (`$ref` cycles) are documented as unsupported, but
39
+ this walker -- like the OpenAI checker it parallels -- does not detect
40
+ reference cycles; it assumes a tree-shaped schema.
41
+ """
42
+ from __future__ import annotations
43
+
44
+ from dataclasses import dataclass
45
+
46
+ from ..paths import join_json_pointer
47
+
48
+ _SUPPORTED_TYPES = frozenset({"string", "number", "integer", "boolean", "array", "object", "null"})
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class SubsetViolation:
53
+ json_pointer: str
54
+ reason: str
55
+
56
+
57
+ def check_strict_subset(schema: dict, *, base_pointer: str = "") -> tuple[SubsetViolation, ...]:
58
+ """Returns every violation of the documented strict-mode subset found in
59
+ `schema`, walked recursively through object properties and array items.
60
+ An empty tuple means the schema is expressible in the strict subset."""
61
+ violations: list[SubsetViolation] = []
62
+ _walk(schema, base_pointer, violations)
63
+ return tuple(violations)
64
+
65
+
66
+ def _walk(schema: dict, pointer: str, violations: list[SubsetViolation]) -> None:
67
+ if not isinstance(schema, dict):
68
+ violations.append(SubsetViolation(pointer, "schema node must be a JSON object"))
69
+ return
70
+
71
+ schema_type = schema.get("type")
72
+ type_names = schema_type if isinstance(schema_type, list) else [schema_type]
73
+ unsupported = [t for t in type_names if t not in _SUPPORTED_TYPES]
74
+ if unsupported:
75
+ violations.append(
76
+ SubsetViolation(pointer, f"unsupported type(s) {unsupported} outside {sorted(_SUPPORTED_TYPES)}")
77
+ )
78
+
79
+ if "object" in type_names:
80
+ if schema.get("additionalProperties") is not False:
81
+ violations.append(SubsetViolation(pointer, "object schemas must set additionalProperties: false"))
82
+
83
+ properties: dict = schema.get("properties", {})
84
+ required = set(schema.get("required", []))
85
+ missing_required = sorted(set(properties) - required)
86
+ if missing_required:
87
+ violations.append(
88
+ SubsetViolation(
89
+ pointer,
90
+ f"all properties must be listed in required (missing: {missing_required}); "
91
+ f"express optional fields as a nullable type union instead",
92
+ )
93
+ )
94
+ for prop_name, prop_schema in properties.items():
95
+ _walk(prop_schema, join_json_pointer(join_json_pointer(pointer, "properties"), prop_name), violations)
96
+
97
+ if "array" in type_names:
98
+ items = schema.get("items")
99
+ if items is None:
100
+ violations.append(SubsetViolation(pointer, "array schemas must declare items"))
101
+ else:
102
+ _walk(items, join_json_pointer(pointer, "items"), violations)