Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion forge_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ def ref_cmd(
raise typer.Exit(1)

indent = None if compact else 2
typer.echo(json.dumps(incident.to_ref_envelope(), indent=indent))
typer.echo(json.dumps(incident.to_ref_envelope_model().to_dict(), indent=indent))


@app.command()
Expand Down
2 changes: 1 addition & 1 deletion forge_cli/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def forge_incident_ref(incident_id: str) -> str:
if incident is None:
return f"No incident found matching '{incident_id}'."

return json.dumps(incident.to_ref_envelope(), indent=2)
return json.dumps(incident.to_ref_envelope_model().to_dict(), indent=2)


@mcp.tool()
Expand Down
43 changes: 32 additions & 11 deletions forge_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,23 @@ def to_dict(self) -> dict[str, Any]:
return asdict(self)


@dataclass(frozen=True)
class IncidentRefEnvelope:
"""Typed Proofhouse shared-contract envelope for Forge IncidentRef output."""

contract_version: str
contract_name: str
producer_capability: str
producer_system: str
canonical_owner: str
issued_at: str
cache_policy: str
ref: IncidentRef

def to_dict(self) -> dict[str, Any]:
return asdict(self)


@dataclass
class Incident:
id: str
Expand Down Expand Up @@ -549,6 +566,10 @@ def to_ref(self) -> IncidentRef:

def to_ref_envelope(self) -> dict[str, Any]:
"""Project this incident into a Proofhouse V0.1 ref envelope."""
return self.to_ref_envelope_model().to_dict()

def to_ref_envelope_model(self) -> IncidentRefEnvelope:
"""Project this incident into a typed Proofhouse V0.1 ref envelope."""
return build_incident_ref_envelope(self)

@classmethod
Expand Down Expand Up @@ -652,15 +673,15 @@ def build_incident_ref(incident: Incident) -> IncidentRef:
)


def build_incident_ref_envelope(incident: Incident) -> dict[str, Any]:
def build_incident_ref_envelope(incident: Incident) -> IncidentRefEnvelope:
"""Wrap an IncidentRef in the Proofhouse shared-contract envelope."""
return {
"contract_version": PROOFHOUSE_SHARED_CONTRACT_VERSION,
"contract_name": "IncidentRef",
"producer_capability": "forge",
"producer_system": FORGE_PRODUCER_SYSTEM,
"canonical_owner": "forge",
"issued_at": incident.timestamp,
"cache_policy": "summary_snapshot",
"ref": build_incident_ref(incident).to_dict(),
}
return IncidentRefEnvelope(
contract_version=PROOFHOUSE_SHARED_CONTRACT_VERSION,
contract_name="IncidentRef",
producer_capability="forge",
producer_system=FORGE_PRODUCER_SYSTEM,
canonical_owner="forge",
issued_at=incident.timestamp,
cache_policy="summary_snapshot",
ref=build_incident_ref(incident),
)
12 changes: 12 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
PROOFHOUSE_SHARED_CONTRACT_VERSION,
FailureType,
Incident,
IncidentRefEnvelope,
ISSUE_CLASS_VALUES,
Severity,
parse_observed_state,
Expand Down Expand Up @@ -288,6 +289,17 @@ def test_incident_ref_envelope_shape(sample_data):
assert envelope["ref"]["organization_id"] == FORGE_UNSCOPED_ORGANIZATION_ID


def test_incident_ref_envelope_has_typed_boundary_model(sample_data):
incident = Incident.from_dict(sample_data)
envelope = incident.to_ref_envelope_model()

assert isinstance(envelope, IncidentRefEnvelope)
assert envelope.contract_name == "IncidentRef"
assert envelope.producer_capability == "forge"
assert envelope.ref.incident_id == sample_data["id"]
assert envelope.to_dict() == incident.to_ref_envelope()


def test_structured_document_operations_incident_roundtrip(sample_data):
data = sample_data.copy()
data.update(
Expand Down
Loading