proofhouse 0.4.1__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.
- proofhouse/__init__.py +9 -0
- proofhouse/cli.py +140 -0
- proofhouse/compiler/__init__.py +12 -0
- proofhouse/compiler/adapters/__init__.py +56 -0
- proofhouse/compiler/adapters/anthropic.py +305 -0
- proofhouse/compiler/adapters/anthropic_schema_subset.py +102 -0
- proofhouse/compiler/adapters/base.py +67 -0
- proofhouse/compiler/adapters/fake.py +107 -0
- proofhouse/compiler/adapters/gemini.py +344 -0
- proofhouse/compiler/adapters/gemini_schema_subset.py +143 -0
- proofhouse/compiler/adapters/openai.py +219 -0
- proofhouse/compiler/adapters/openai_schema_subset.py +85 -0
- proofhouse/compiler/api.py +384 -0
- proofhouse/compiler/benchmark.py +367 -0
- proofhouse/compiler/canonical.py +97 -0
- proofhouse/compiler/capability.py +55 -0
- proofhouse/compiler/cli_compiler.py +887 -0
- proofhouse/compiler/closed_loop.py +642 -0
- proofhouse/compiler/codegen/__init__.py +0 -0
- proofhouse/compiler/codegen/typescript.py +126 -0
- proofhouse/compiler/contracts.py +293 -0
- proofhouse/compiler/diagnostics.py +159 -0
- proofhouse/compiler/eval_aggregate.py +28 -0
- proofhouse/compiler/eval_dataset.py +80 -0
- proofhouse/compiler/eval_product.py +184 -0
- proofhouse/compiler/eval_rubric.py +61 -0
- proofhouse/compiler/evaluation.py +79 -0
- proofhouse/compiler/evidence.py +89 -0
- proofhouse/compiler/execution.py +441 -0
- proofhouse/compiler/hosted_openapi.py +158 -0
- proofhouse/compiler/hosted_slice.py +270 -0
- proofhouse/compiler/immutability.py +71 -0
- proofhouse/compiler/intake.py +213 -0
- proofhouse/compiler/ir.py +104 -0
- proofhouse/compiler/missionrig.py +148 -0
- proofhouse/compiler/model_suggest.py +89 -0
- proofhouse/compiler/orchestration/__init__.py +1 -0
- proofhouse/compiler/orchestration/assay.py +30 -0
- proofhouse/compiler/orchestration/claim_ledger.py +21 -0
- proofhouse/compiler/orchestration/learning.py +111 -0
- proofhouse/compiler/orchestration/overseer.py +9 -0
- proofhouse/compiler/orchestration/proof.py +50 -0
- proofhouse/compiler/orchestration/route.py +120 -0
- proofhouse/compiler/orchestration/shipwalk_consume.py +26 -0
- proofhouse/compiler/orchestration/stack_profiles.py +77 -0
- proofhouse/compiler/passes/__init__.py +20 -0
- proofhouse/compiler/passes/adapter_lowering.py +159 -0
- proofhouse/compiler/passes/base.py +46 -0
- proofhouse/compiler/passes/capability_resolution.py +103 -0
- proofhouse/compiler/passes/normalization.py +32 -0
- proofhouse/compiler/passes/optimization.py +17 -0
- proofhouse/compiler/passes/safety.py +69 -0
- proofhouse/compiler/passes/validation.py +68 -0
- proofhouse/compiler/paths.py +55 -0
- proofhouse/compiler/pipeline.py +76 -0
- proofhouse/compiler/plain_language.py +134 -0
- proofhouse/compiler/repair.py +47 -0
- proofhouse/compiler/requirements_contract.py +774 -0
- proofhouse/compiler/requirements_ir_bridge.py +222 -0
- proofhouse/compiler/requirements_plain_produce.py +193 -0
- proofhouse/compiler/requirements_produce.py +181 -0
- proofhouse/compiler/resource_bounds.py +8 -0
- proofhouse/compiler/runtime_context.py +235 -0
- proofhouse/compiler/schemas/benchmark-manifest.schema.json +108 -0
- proofhouse/compiler/schemas/diagnostic_code_registry.json +30 -0
- proofhouse/compiler/schemas/diagnostic_contract.schema.json +47 -0
- proofhouse/compiler/schemas/proofhouse_ir_v0_1.schema.json +217 -0
- proofhouse/compiler/schemas/requirements_diagnostic_registry.json +35 -0
- proofhouse/compiler/sink.py +55 -0
- proofhouse/loadouts.py +110 -0
- proofhouse/optimize/__init__.py +5 -0
- proofhouse/optimize/case.py +444 -0
- proofhouse/optimize/checks.py +416 -0
- proofhouse/optimize/cli.py +759 -0
- proofhouse/optimize/cli_workflow.py +342 -0
- proofhouse/optimize/data/model_registry.json +253 -0
- proofhouse/optimize/data/proofhouse-framework.json +402 -0
- proofhouse/optimize/data/proofhouse.skill +0 -0
- proofhouse/optimize/data/reference-advisory/advisory.txt +10 -0
- proofhouse/optimize/data/reference-advisory/answers.json +5 -0
- proofhouse/optimize/data/reference-advisory/objective.txt +1 -0
- proofhouse/optimize/data/reference-advisory/output-v1.txt +1 -0
- proofhouse/optimize/data/reference-advisory/output-v2.txt +1 -0
- proofhouse/optimize/data/reference-advisory/prompt-v1.txt +1 -0
- proofhouse/optimize/data/reference-advisory/prompt-v2.txt +1 -0
- proofhouse/optimize/install_skill.py +302 -0
- proofhouse/optimize/model_notes.py +296 -0
- proofhouse/optimize/packets.py +182 -0
- proofhouse/optimize/reference_workflow.py +218 -0
- proofhouse/optimize/registry.py +114 -0
- proofhouse/optimize/workflow.py +507 -0
- proofhouse/runner.py +66 -0
- proofhouse/scan.py +351 -0
- proofhouse/schemas.py +51 -0
- proofhouse/scoring.py +30 -0
- proofhouse/templates.py +168 -0
- proofhouse-0.4.1.dist-info/METADATA +260 -0
- proofhouse-0.4.1.dist-info/RECORD +102 -0
- proofhouse-0.4.1.dist-info/WHEEL +5 -0
- proofhouse-0.4.1.dist-info/entry_points.txt +3 -0
- proofhouse-0.4.1.dist-info/licenses/LICENSE +21 -0
- proofhouse-0.4.1.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)
|